Tipos Numéricos em Golang

GolangBeginner
Pratique Agora

Introdução

Bem-vindos Gophers a este novo capítulo. Nesta seção, aprenderemos sobre tipos numéricos. O conteúdo inclui tipos inteiros comumente usados, tipos de ponto flutuante, tipos booleanos, bem como números complexos e a sintaxe de valor literal introduzida na versão 1.13.

Pontos de Conhecimento:

  • Tipos inteiros
  • Tipos de ponto flutuante (floating-point types)
  • Tipos booleanos
  • Números complexos
  • Sintaxe de valor literal (literal value syntax)

Inteiros

Os inteiros podem ser amplamente divididos em duas categorias: inteiros sem sinal (unsigned integers) e inteiros com sinal (signed integers). Os inteiros com sinal são os mais amplamente utilizados.

Sem sinal (unsigned) significa que pode representar apenas números não negativos (0 e números positivos), enquanto os números com sinal (signed) podem representar números negativos e não negativos.

Os inteiros sem sinal podem ser divididos em quatro tamanhos: 8 bits, 16 bits, 32 bits e 64 bits, representados por uint8, uint16, uint32 e uint64, respectivamente. Os inteiros com sinal correspondentes são int8, int16, int32 e int64. A tabela a seguir mostra as diferentes faixas representadas por cada tipo:

Tipo Descrição Faixa
uint8 Inteiro sem sinal de 8 bits 0 a 255
int8 Inteiro com sinal de 8 bits -128 a 127
uint16 Inteiro sem sinal de 16 bits 0 a 65535
int16 Inteiro com sinal de 16 bits -32768 a 32767
uint32 Inteiro sem sinal de 32 bits 0 a 4294967295
int32 Inteiro com sinal de 32 bits -2147483648 a 2147483647
uint64 Inteiro sem sinal de 64 bits 0 a 18446744073709551615
int64 Inteiro com sinal de 64 bits -9223372036854775808 a 9223372036854775807

Vamos tomar uint8 e int8 como exemplos. Ambos são inteiros de 8 bits e podem representar 256 valores. No tipo de inteiro sem sinal uint8, a faixa que ele pode representar é de 0 a 255, enquanto no tipo de inteiro com sinal int8, a faixa que ele pode representar é de -128 a 127.

Além dos 8 tipos acima, existem três outros tipos de inteiros especiais, uint, int e uintptr, onde uint e int podem representar diferentes faixas em diferentes plataformas, e uintptr é usado para armazenar endereços de ponteiros.

Tipo Faixa
uint uint32 em sistemas de 32 bits, uint64 em sistemas de 64 bits
int int32 em sistemas de 32 bits, int64 em sistemas de 64 bits
uintptr Tipo de inteiro sem sinal usado para armazenar endereços de ponteiros, usado principalmente em programação de baixo nível, como operações inseguras

Agora, vamos criar um arquivo chamado integer.go para demonstrar o uso de inteiros:

cd ~/project
touch integer.go
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    // View the type of int in the current environment
    // Declare a as the type int
    var a int
    // Use unsafe.Sizeof() to output the memory size occupied by the type
    fmt.Printf("The type int in the current environment is %d bits\n", unsafe.Sizeof(a)*8)

    var b int8 = 125
    // Use the %d placeholder in fmt.Printf to output the value of the integer
    // Use the %T placeholder in fmt.Printf to output the type of the variable
    fmt.Printf("The value of b is %d, and the type is %T\n", b, b)

    // Integer operations
    // Declare integers c and d, and calculate their sum
    c, d := 2, 3
    fmt.Printf("c + d = %d\n", c+d)
    // 10 - 5
    fmt.Printf("10 - 5 = %d\n", 10-5)
    // 8 * 10
    fmt.Printf("8 * 10 = %d\n", 8*10)

    // Example of uintptr usage - storing an address
    var ptr uintptr
    x := 42
    // Convert the pointer to x to uintptr
    ptr = uintptr(unsafe.Pointer(&x))
    fmt.Printf("The address of x stored in ptr is: %v\n", ptr)
}

Após executar o programa, obtemos a seguinte saída:

go run integer.go
The type int in the current environment is 64 bits
The value of b is 125, and the type is int8
c + d = 5
10 - 5 = 5
8 * 10 = 80
The address of x stored in ptr is: 824634818784

Explicação da saída:

  • The type int in the current environment is 64 bits: Esta linha mostra que o tipo int no sistema onde o código está sendo executado é de 64 bits (ou 8 bytes), indicando que é um int64. unsafe.Sizeof(a) retorna o tamanho da variável a em bytes, e multiplicar por 8 converte para bits. Isso significa que o tipo int pode armazenar valores inteiros maiores.
  • The value of b is 125, and the type is int8: Aqui, declaramos uma variável b do tipo int8 e atribuímos o valor 125. A saída confirma isso mostrando tanto o valor quanto o tipo de dados.
  • c + d = 5, 10 - 5 = 5, 8 * 10 = 80: Estas linhas mostram operações aritméticas básicas com inteiros: adição, subtração e multiplicação. A saída confirma os resultados corretos dessas operações.
  • The address of x stored in ptr is: 824634818784: Isso demonstra como uintptr pode ser usado para armazenar um endereço de memória. Estamos convertendo um ponteiro para a variável inteira x para um tipo uintptr. O valor real do endereço variará cada vez que o programa for executado. Este é um caso de uso avançado, normalmente encontrado em operações inseguras e programação de sistemas.

Neste arquivo, a função unsafe.Sizeof() pode ser usada para obter o número de bytes ocupados pelo tipo de variável atual. 1 byte (byte) é igual a 8 bits, então unsafe.Sizeof()*8 pode obter o número de bits ocupados pelo tipo. Pela saída, podemos ver que o ambiente online é de 64 bits. O tipo real de int no ambiente online é int64.

Podemos usar o seguinte comando no terminal para determinar a arquitetura do sistema atual:

dpkg --print-architecture
amd64

Números de Ponto Flutuante

Os números de ponto flutuante representam números reais com partes fracionárias em Go usando dois tipos: float32 e float64. O tipo de ponto flutuante padrão é float64.

float32 e float64 representam diferentes precisões. A precisão padrão de float64 é maior do que a de float32.

O padrão IEEE 754 é o padrão de cálculo de ponto flutuante mais amplamente usado em computadores, e as CPUs modernas suportam esse padrão. Como outras linguagens de programação, Go também usa IEEE 754 para armazenar números de ponto flutuante.

Sabemos que um byte em um computador pode armazenar 8 bits. float32 é um número de ponto flutuante de precisão simples e ocupa 4 bytes, que são 32 bits. float64 é de precisão dupla e ocupa 8 bytes, que são 64 bits.

Em float32, o bit de sinal ocupa 1 bit, o expoente ocupa 8 bits e os 23 bits restantes são usados para representar a mantissa.

Em float64, o bit de sinal também ocupa 1 bit, o expoente ocupa 11 bits e os 52 bits restantes são usados para representar a mantissa.

O valor máximo que float32 pode representar é aproximadamente 3.4e+38 em notação científica, e o valor mínimo é 1.4e-45. O valor máximo que float64 pode representar é aproximadamente 1.8e+308, e o valor mínimo é 4.9e-324. Podemos ver que a faixa de valores de ponto flutuante pode ser de muito pequeno a muito grande.

Podemos usar a constante math.MaxFloat32 para representar o valor máximo em float32. Use a constante math.MaxFloat64 para representar o valor máximo em float64.

Representação de Números de Ponto Flutuante

Ao exibir números de ponto flutuante, podemos usar o marcador %f da função Printf do pacote fmt. Aqui está um exemplo:

cd ~/project
touch float.go
package main

import (
    "fmt"
    "math"
)

func main() {
    // Output without exponential form
    fmt.Printf("2.333 without exponential form: %f\n", 2.333)
    fmt.Printf("Pi without exponential form: %f\n", math.Pi)
    // Use %.2f to keep two decimal places for Pi
    fmt.Printf("Pi with two decimal places: %.2f\n", math.Pi)
    fmt.Printf("The maximum value of float32: %f\n", math.MaxFloat32)
    // Exponential form
    fmt.Printf("2.333 in exponential form: %e", 2.333)
}

Execute o comando e você verá a seguinte saída.

go run float.go
2.333 without exponential form: 2.333000
Pi without exponential form: 3.141593
Pi with two decimal places: 3.14
The maximum value of float32: 340282346638528859811704183484516925440.000000
2.333 in exponential form: 2.333000e+00

Explicação da saída:

  • 2.333 without exponential form: 2.333000: Isso mostra o número 2.333 impresso usando o marcador %f. Por padrão, o marcador %f mostra 6 dígitos após a vírgula decimal, então 2.333 se torna 2.333000.
  • Pi without exponential form: 3.141593: Esta linha imprime o valor da constante math.Pi, que é uma aproximação da constante matemática π. O marcador %f o exibe com precisão total.
  • Pi with two decimal places: 3.14: Usando %.2f, dizemos a fmt.Printf para formatar o número com duas casas decimais, o que resulta em 3.14. Isso é muito útil quando você só precisa de uma certa precisão para a saída.
  • The maximum value of float32: 340282346638528859811704183484516925440.000000: Isso mostra o valor máximo que um float32 pode representar. Observe que é um número muito grande e, quando impresso com %f, será representado com muitos dígitos.
  • 2.333 in exponential form: 2.333000e+00: Esta linha mostra como representar um número de ponto flutuante em notação exponencial (científica) usando o marcador %e. O e+00 no final indica que multiplicamos 2.333000 por 10 elevado à potência de 0, que é simplesmente 2.333. Se o expoente fosse e+02, então o número seria multiplicado por 10^2 (100), resultando em 233.3.

Tipos Booleanos

O tipo bool tem apenas dois valores possíveis: true ou false, sendo false o padrão. Ele tem as seguintes características:

  • Não pode ser convertido para outros tipos, como converter um inteiro em um booleano ou converter um booleano em um inteiro.
  • Não pode participar de operações aritméticas.

Tipos booleanos são geralmente usados em conjunto com operadores relacionais, como =, >, e <. Vamos criar um arquivo chamado bool.go para ver uma demonstração:

cd ~/project
touch bool.go
package main

import (
    "fmt"
)

func main() {
    // Use the %t placeholder in fmt.Printf to represent a boolean value
    fmt.Printf("Is 3 equal to 2? %t\n", 3 == 2)
    fmt.Printf("Is 2 equal to 2? %t\n", 2 == 2)

    // Determine whether a and b are equal
    a, b := 1, 2
    fmt.Printf("a is %d, b is %d\n", a, b)
    if a > b {
        fmt.Println("a is greater than b")
    } else if a == b {
        fmt.Println("a is equal to b")
    } else {
        fmt.Println("b is greater than a")
    }
}

Após executar o programa, obtemos a seguinte saída:

go run bool.go
Is 3 equal to 2? false
Is 2 equal to 2? true
a is 1, b is 2
b is greater than a

Explicação da saída:

  • Is 3 equal to 2? false: A expressão 3 == 2 avalia para false, que é então impresso usando o marcador %t.
  • Is 2 equal to 2? true: A expressão 2 == 2 avalia para true, que é então impresso usando o marcador %t.
  • a is 1, b is 2: Esta linha imprime os valores atribuídos às variáveis inteiras a e b.
  • b is greater than a: A instrução condicional if-else if-else avalia para a < b, que é 1 < 2 (verdadeiro), portanto, a instrução else é executada, resultando no programa imprimindo "b is greater than a".

Neste programa, primeiro usamos %t em fmt.Printf para representar um valor booleano e, em seguida, usamos a instrução if para determinar a relação entre dois valores. Em fmt.Printf, podemos usar o marcador %d para representar um inteiro, o marcador %f para representar um número de ponto flutuante e o marcador %t para representar um valor booleano.

Números Complexos

Go também possui tipos de números complexos embutidos, que podem ser divididos em tipos complex64 e complex128. Em complex64, tanto a parte real quanto a imaginária são de 32 bits, enquanto em complex128, ambas as partes real e imaginária são de 64 bits. Podemos facilmente realizar operações com números complexos em Go.

Crie um arquivo chamado complex.go e insira o seguinte código:

cd ~/project
touch complex.go
package main

import (
    "fmt"
)

func main() {
    // Initialize complex numbers in different ways
    c1 := complex(3, 1)
    c2 := 4 + 5i

    // Complex number operations
    c3 := c1 + c2
    c4 := c1 * c2
    // Use the real() function to obtain the real part of a complex number, and the imag() function to obtain the imaginary part of a complex number
    fmt.Printf("The real part of c1 is %v, the imaginary part is %v\n", real(c1), imag(c1))
    // %v in fmt.Printf can be used to represent complex numbers
    fmt.Printf("c1 + c2 is %v\n", c3)
    fmt.Printf("c1 * c2 is %v\n", c4)
}

Após executar o programa, obtemos a seguinte saída:

go run complex.go
The real part of c1 is 3, the imaginary part is 1
c1 + c2 is (7+6i)
c1 * c2 is (7+19i)

Explicação da saída:

  • The real part of c1 is 3, the imaginary part is 1: A função real(c1) extrai a parte real do número complexo c1, que é 3, e imag(c1) extrai a parte imaginária de c1, que é 1. Esses valores são então impressos usando o marcador %v.
  • c1 + c2 is (7+6i): Esta linha mostra o resultado da adição dos números complexos c1 e c2. c1 é definido como 3 + 1i e c2 como 4 + 5i. Adicionar as partes real e imaginária separadamente dá (3 + 4) + (1 + 5)i, ou 7 + 6i.
  • c1 * c2 is (7+19i): Esta linha mostra o resultado da multiplicação dos números complexos c1 e c2. (3 + 1i) * (4 + 5i) é calculado como (3*4 - 1*5) + (3*5 + 1*4)i, que simplifica para (12-5) + (15+4)i, e finalmente para 7+19i.

Neste programa, demonstramos como inicializar e realizar operações em números complexos usando a função complex e os operadores + e *. real() e imag() são usados para extrair as partes real e imaginária de um número complexo, respectivamente. O verbo %v em fmt.Printf é usado como um marcador geral para exibir números complexos.

Sintaxe de Valores Literais

Na versão 1.13, Go introduziu a Numeric Literal Syntax (Sintaxe Literal Numérica). Ela define a representação de números em diferentes sistemas numéricos. Vamos dar uma olhada em suas regras.

  • Binário: Adicione 0b antes do inteiro. Por exemplo, 0b101 é equivalente a 5 em decimal.
  • Octal: Adicione 0o ou 0O antes do inteiro. Por exemplo, 0o11 é equivalente a 9 em decimal.
  • Hexadecimal: Adicione 0x ou 0X antes do inteiro. Por exemplo, 0x1b é equivalente a 27 em decimal.
  • Use _ para separar dígitos no inteiro, por exemplo, 0b1000_0100_0010_0001 é equivalente a 0b1000010000100001. Isso pode melhorar a legibilidade.

Vamos demonstrá-lo em detalhes:

cd ~/project
touch literals.go
package main

import "fmt"

func main() {
    // Binary, add 0b at the beginning
    var a int = 0b101
    fmt.Printf("Binary a is %b, decimal is %d\n", a, a)

    // Octal, add 0o or 0O at the beginning
    var b int = 0o11
    fmt.Printf("Octal b is %o, decimal is %d\n", b, b)

    // Hexadecimal, add 0x or 0X at the beginning
    var c int = 0x1b
    fmt.Printf("Hexadecimal c is %x, decimal is %d\n", c, c)

    // Use separators
    d := 0b1000_0100_0010_0001
    e := 0b1000010000100001
    if d == e {
        fmt.Println("d is equal to e")
    }
}

Após executar o programa, obtemos a seguinte saída:

go run literals.go
Binary a is 101, decimal is 5
Octal b is 11, decimal is 9
Hexadecimal c is 1b, decimal is 27
d is equal to e

Explicação da saída:

  • Binary a is 101, decimal is 5: Esta linha mostra a representação binária de 0b101, que é 101, e seu equivalente decimal é 5. O marcador %b exibe o valor binário de a, enquanto %d exibe a representação decimal.
  • Octal b is 11, decimal is 9: O número octal 0o11 é equivalente a 1*8^1 + 1*8^0 = 8 + 1 = 9 em decimal. O marcador %o exibe a representação octal de b, e %d exibe seu valor decimal.
  • Hexadecimal c is 1b, decimal is 27: O número hexadecimal 0x1b é equivalente a 1*16^1 + 11*16^0 = 16 + 11 = 27 em decimal. O marcador %x exibe a representação hexadecimal de c, e %d exibe seu valor decimal.
  • d is equal to e: Isso mostra que o literal binário 0b1000_0100_0010_0001 é igual a 0b1000010000100001 em valor. Os sublinhados são puramente para legibilidade e não alteram o valor do número. A condição if avalia corretamente para true.

Neste programa, demonstramos a declaração e a saída de diferentes sistemas numéricos, bem como o uso de separadores. Em fmt.Printf, usamos diferentes marcadores para representar diferentes sistemas numéricos. Por exemplo, %b representa binário e %x representa hexadecimal. Dominá-los melhorará a eficiência da programação.

Resumo

Vamos rever o que aprendemos nesta seção:

  • Inteiros podem ser divididos em inteiros com sinal e inteiros sem sinal
  • Os tipos de inteiro padrão int e uint têm intervalos que dependem da plataforma
  • Representação de números de ponto flutuante
  • Como usar números complexos
  • Introdução da sintaxe de valor literal
  • Como usar diferentes marcadores

Nesta seção, explicamos e demonstramos tipos de inteiros comuns, tipos de ponto flutuante e tipos booleanos. Discutimos seus tamanhos, intervalos e uso. Também introduzimos números complexos e constantes literais. Tipos numéricos são a pedra angular dos programas Go, e é importante que os alunos os estudem bem.