Entendendo os tipos de dados em Go

Introdução

Os tipos de dados especificam os tipos de valores que variáveis específicas armazenarão quando estiver escrevendo um programa. O tipo de dados também determina quais operações podem ser realizadas nos dados.

Neste artigo, vamos ver os tipos de dados importantes nativos para Go. Esta não é uma investigação exaustiva dos tipos de dados, mas ajudará você a se familiarizar com as opções disponíveis em Go. Entender alguns tipos básicos de dados permitirá que você escreva um código mais claro, que apresente desempenho eficiente.

Contexto

Uma forma de pensar sobre os tipos de dados é considerar os diferentes tipos de dados que usamos no mundo real. Um exemplo de dados no mundo real são números: podemos usar números naturais (0, 1, 2, …), inteiros (…, -1, 0, 1, …) e números irracionais (π), por exemplo.

Normalmente, em matemática, podemos combinar números de tipos diferentes e obter algum tipo de resposta. Podemos querer adicionar 5 a π, por exemplo:

5 + π 

Ou podemos manter a equação como a resposta para levar em conta o número irracional, ou arredondar π para um número com uma quantidade reduzida de casas decimais, para, depois, somar os números:

5 + π = 5 + 3.14 = 8.14 

Mas,se começarmos a tentar avaliar os números com outro tipo de dados, como palavras, as coisas começam a fazer menos sentido. Como resolveríamos a seguinte equação?

shark + 8 

Para os computadores, cada tipo de dados é bastante diferente, como palavras e números. Consequentemente, precisamos ter cuidado sobre como usamos tipos de dados diferentes para atribuir valores e como os manipulamos nas operações.

Números inteiros

Assim como ocorre na matemática, os números inteiros na programação de computadores são números inteiros que podem ser positivos, negativos, ou 0 (…, -1, 0, 1, …). Em Go, um inteiro é conhecido como int. Como com outras linguagens de programação, não se deve usar pontos em números de quatro dígitos ou mais, então quando for escrever 1.000 no seu programa, escreva como 1000.

Podemos imprimir um inteiro de uma forma simples assim:

fmt.Println(-459) 
Output-459 

Ou, podemos declarar uma variável que, neste caso, é um símbolo do número que estamos usando ou manipulando, desta forma:

var absoluteZero int = -459 fmt.Println(absoluteZero) 
Output-459 

Também podemos fazer operações matemáticas com inteiros em Go. No bloco de código a seguir, usaremos o operador de atribuição := para declarar e instanciar a variável sum:

sum := 116 - 68 fmt.Println(sum) 
Output48 

Como mostra o resultado, o operador matemático - subtraiu o número inteiro 68 de 116, resultando em 48. Você irá aprender mais sobre a declaração de variáveis na seção Declarando tipos de dados para variáveis.

Os inteiros podem ser usados de várias maneiras dentro de programas em Go. Conforme você for aprendendo mais sobre a linguagem Go, você terá muitas oportunidades de trabalhar com inteiros e desenvolver seus conhecimentos sobre esse tipo de dado.

Números de ponto flutuante

Um número de ponto flutuante ou float é usado para representar números reais que não podem ser expressos como inteiros. Os números reais incluem todos os números racionais e irracionais e, por isso, os números de ponto flutuante podem conter uma parte fracionada, como 9.0 ou -116.42. [TN: embora na língua portuguesa nós utilizemos a “,” (vírgula) como separador de casas decimais, vamos manter a notação usada em inglês, ou seja, usaremos o “.” (ponto) como separador de casas decimais, a fim de evitar conflitos com a programação.] Para a finalidade de considerarmos um float em um programa em Go, podemos dizer que se trata de um número contendo um ponto como separador decimal.[TN: please see prior comment on this.]

Como fizemos com os números inteiros, podemos imprimir um número de ponto flutuante de uma forma simples assim:

fmt.Println(-459.67) 
Output-459.67 

Também podemos declarar uma variável que identifica um float, desta forma:

absoluteZero := -459.67 fmt.Println(absoluteZero) 
Output-459.67 

Assim como com inteiros, também podemos fazer matemática com floats em Go:

var sum = 564.0 + 365.24 fmt.Println(sum) 
Output929.24 

Com os números inteiros e os números de ponto flutuante, é importante ter em mente que 3 ≠ 3.0, já que 3 refere-se a um inteiro enquanto 3.0 se refere a um float.

Tamanhos dos tipos numéricos

Além da distinção entre inteiros e floats, a linguagem Go tem dois tipos de dados numéricos que são distinguidos pela natureza estática ou dinâmica dos seus tamanhos. O primeiro tipo é um tipo independente de arquitetura, o que significa que o tamanho dos dados em bits não é alterado, independentemente da máquina em que o código estiver executando.

Atualmente, a maioria das arquiteturas dos sistemas é de 32 bits ou 64 bits. Por exemplo, você pode estar desenvolvendo para um notebook moderno em Windows, no qual o sistema operacional executa uma arquitetura de 64 bits. No entanto, se estiver desenvolvendo um dispositivo como um relógio fitness, você pode estar trabalhando com uma arquitetura de 32 bits. Caso use um tipo independente de arquitetura como int32, independentemente da arquitetura para a qual você compilar, o tipo terá um tamanho constante.

O segundo tipo é um tipo específico da implementação. Nesse tipo, o tamanho do bit pode variar, dependendo da arquitetura em que o programa foi compilado. Por exemplo, se usarmos o tipo int, quando Go compilar para uma arquitetura de 32 bits, o tamanho do tipo de dados será de 32 bits. Se o programa for compilado para uma arquitetura de 64 bits, a variável terá um tamanho de 64 bits.

Além dos tipos de dados terem tamanhos diferentes, tipos como inteiros também vêm em dois tipos básicos: assinados e não assinados. Um int8 é um número inteiro assinado e pode assumir valores entre -128 e 127. Um uint8 é um número inteiro não assinado e apenas pode ter valores positivos entre 0 a 255.

Os intervalos se baseiam no tamanho do bit. Para os dados binários, 8 bits podem representar um total de 256 valores diferentes. Como um tipo int precisa oferecer suporte tanto a valores positivos como a negativos, um inteiro de 8 bits (int8) terá um intervalo entre -128 a 127, para um total de 256 valores únicos possíveis.

A linguagem Go tem os seguintes tipos de inteiros independentes de arquitetura:

uint8       unsigned  8-bit integers (0 to 255) uint16      unsigned 16-bit integers (0 to 65535) uint32      unsigned 32-bit integers (0 to 4294967295) uint64      unsigned 64-bit integers (0 to 18446744073709551615) int8        signed  8-bit integers (-128 to 127) int16       signed 16-bit integers (-32768 to 32767) int32       signed 32-bit integers (-2147483648 to 2147483647) int64       signed 64-bit integers (-9223372036854775808 to 9223372036854775807) 

Os floats e os números complexos também vêm em diferentes tamanhos:

float32     IEEE-754 32-bit floating-point numbers float64     IEEE-754 64-bit floating-point numbers complex64   complex numbers with float32 real and imaginary parts complex128  complex numbers with float64 real and imaginary parts 

Também há alguns tipos de alias (pseudônimos) de números, os quais atribuem nomes úteis a tipos de dados específicos:

byte        alias for uint8 rune        alias for int32 

A finalidade do alias do byte é deixar claro quando o seu programa está usando bytes como uma medição computacional comum em elementos de string de caracteres, ao contrário do que ocorre com números inteiros pequenos e não relacionados com a medição de dados byte. Embora o byte e uint8 fiquem idênticos assim que o programa está compilado, com frequência, o byte é usado para representar dados de caracteres em forma numérica, ao passo que o objetivo do uint8 ser um número em seu programa.

O alias rune é um pouco diferente. Enquanto o byte e o uint8 são exatamente os mesmos dados, um rune pode ser um único byte ou quatro bytes, um intervalo determinado pelo int32. Um rune é usado para representar um caractere Unicode, enquanto os caracteres ASCII podem ser representados apenas por um tipo de dados int32.

Além disso, a linguagem Go tem os seguintes tipos específicos de implementação:

uint     unsigned, either 32 or 64 bits int      signed, either 32 or 64 bits uintptr  unsigned integer large enough to store the uninterpreted bits of a pointer value 

Os tipos específicos de implementação terão seu tamanho definido pela arquitetura para a qual o programa foi compilado.

Escolhendo tipos de dados numéricos

Escolher o tamanho correto geralmente tem mais a ver com o desempenho para a arquitetura alvo para a qual está programando, do que o tamanho dos dados com os quais está trabalhando. No entanto, sem a necessidade de conhecer as implicações específicas do desempenho para o seu programa, você pode seguir algumas dessas diretrizes básicas quando estiver começando.

Como discutimos anteriormente neste artigo, há tipos independentes da arquitetura e tipos específicos da implementação. Para os dados de números inteiros, é comum em Go usar tipos da implementação como o int ou uint, em vez de int64 ou uint64. Normalmente, isso resultará em velocidade de processamento mais rápida para sua arquitetura alvo. Por exemplo, se você usar um int64 e compilar para uma arquitetura de 32 bits, levará, pelo menos, o dobro do tempo necessário para processar esses valores, uma vez que são necessários ciclos adicionais da CPU para mover os dados pela arquitetura. Se, em vez disso, você tivesse usado um int, o programa definiria o tipo de implementação como uma arquitetura com 32 bits em tamanho para uma arquitetura de 32 bits e seria significativamente mais rápido para processar.

Se você sabe que não vai exceder um intervalo de tamanho específico, então escolher um tipo independente da arquitetura pode aumentar a velocidade e diminuir o uso de memória. Por exemplo, se você souber que seus dados não irão exceder o valor de 100 e será apenas um número positivo, então, escolher um uint8 tornaria seu programa mais eficiente, uma vez que ele precisará de menos memória.

Agora que examinamos alguns dos intervalos possíveis para os tipos de dados numéricos, vamos ver o que acontecerá se excedermos esses intervalos em nosso programa.

Exceder vs. Limitar

A linguagem Go tem capacidade tanto de exceder um número quanto de limitar um número quando você tenta armazenar um valor maior do que o tipo de dado foi concebido para armazenar, dependendo de se o valor for calculado no momento da compilação ou no da execução. Um erro de tempo de compilação acontece quando o programa encontra um erro enquanto ele tenta compilar o programa. Um erro de tempo de execução acontece após o programa estar compilado, enquanto ele está realmente em execução.

No exemplo a seguir, definimos maxUint32 como seu valor máximo:

package main  import "fmt"  func main() {     var maxUint32 uint32 = 4294967295 // Max uint32 size     fmt.Println(maxUint32) } 

Ele será compilado e executado gerando o seguinte resultado:

Output4294967295 

Se adicionarmos 1 ao valor no tempo de execução, ele ficará limitado em 0:

Output0 

Por outro lado, vamos alterar o programa para adicionar 1 à variável quando a atribuirmos, antes do momento da compilação:

package main  import "fmt"  func main() {     var maxUint32 uint32 = 4294967295 + 1     fmt.Println(maxUint32)  } 

No momento da compilação, se o compilador puder determinar que um valor será demasiadamente elevado para se manter no tipo de dados especificado, ele gerará um erro de overflow. Isso significa que o valor calculado é demasiadamente elevado para o tipo de dados que você especificou.

Como o compilador pode determinar que irá exceder o valor, agora ele irá gerar um erro:

Outputprog.go:6:36: constant 4294967296 overflows uint32 

Entender os limites dos seus dados ajudará você a evitar possíveis bugs em seu programa no futuro.

Agora que abordamos tipos numéricos, vamos ver como armazenar valores booleanos.

Booleanos

O tipo de dados booleano pode ser um de dois valores, true [verdadeiro] ou false [falso] e é definido como bool ao declará-lo como um tipo de dados. Os booleanos são usados para representar os valores verdade que estão associados ao ramo lógico da matemática, o qual informa algoritmos na ciência da computação.

Os valores true e false sempre estarão com as letras t e f minúsculas respectivamente, já que eles são identificadores pré-declarados em Go.

Muitas operações em matemática nos dão respostas que são avaliadas como verdadeiras ou falsas:

  • maior que
    • 500 > 100 verdadeiro
    • 1 > 5 falso
  • menor que
    • 200 < 400 verdadeiro
    • 4 < 2 falso
  • igual
    • 5 = 5 verdadeiro
    • 500 = 400 falso

Assim como com números, podemos armazenar um valor booleano em uma variável:

myBool := 5 > 8 

Então, podemos imprimir o valor booleano com uma chamada para a função fmt.Println():

fmt.Println(myBool) 

Como 5 não é maior que 8, receberemos o seguinte resultado:

Outputfalse 

Conforme você for escrevendo mais programas em Go, você se tornará mais familiarizado com o funcionamento dos booleanos e como diferentes funções e operações sendo avaliadas como true ou false podem alterar o curso do programa.

Strings

Uma string é uma sequência de um ou mais caracteres (letras, números, símbolos) que podem ser uma constante ou uma variável. As strings existem entre sinais de crase “”ou aspas duplas“` em Go e têm características diferentes dependendo de quais sinais gráficos utilizar.

Se você usar sinais de crase, estará criando um literal de string bruta. Se você usar aspas duplas, estará criando um literal de string interpretada.

Literais de string bruta

Os literais de string bruta são sequências de caracteres entre sinais de crase, frequentemente chamadas de backticks. Entre aspas, qualquer caractere aparecerá simplesmente como for exibido entre sinais de crase, exceto o caractere da crase em si.

a := `Say "hello" to Go!` fmt.Println(a) 
OutputSay "hello" to Go! 

Geralmente, as barras invertidas são usadas para representar caracteres especiais em strings. Por exemplo, em uma string interpretada, o n representaria uma nova linha em uma string. No entanto, as barras invertidas não têm um significado especial dentro de literais de string bruta:

a := `Say "hello" to Go!n` fmt.Println(a) 

Como a barra invertida não tem um significado especial em um literal de string, na verdade, ela imprimirá o valor n, em vez de criar uma nova linha:

OutputSay "hello" to Go!n 

As literais de string brutas também podem ser usadas para criar strings de várias linhas:

a := `This string is on multiple lines within a single back quote on either side.` fmt.Println(a) 
OutputThis string is on multiple lines within a single back quote on either side. 

Nos blocos de código anteriores, as novas linhas foram literalmente transferidas da entrada para a saída.

Literais de string interpretada

As literais de string interpretada são sequências de caracteres entre aspas duplas, como em "bar". Qualquer caractere pode vir entre aspas, exceto as novas linhas e as aspas duplas sem escape. Para mostrar aspas duplas em uma string interpretada, é possível usar a barra invertida como um caractere de escape, desta forma:

a := "Say "hello" to Go!" fmt.Println(a) 
OutputSay "hello" to Go! 

Quase sempre você vai usar os literais de string interpretada porque eles permitem o escape de caracteres dentro deles. Para obter mais informações sobre o trabalho com strings, leia o artigo Uma introdução ao trabalho com strings em Go.

Strings com caracteres UTF-8

O UTF-8 é um esquema de codificação usado para codificar caracteres de largura variável em um a quatro bytes. A linguagem Go já vem totalmente compatível com os caracteres UTF-8, sem qualquer configuração especial, bibliotecas ou pacotes. Caracteres romanos – como a letra A, podem ser representados por um valor do código ASCII, como o número 65. No entanto, com caracteres especiais como um caractere internacional , seria necessário ter o UTF-8 instalado. A linguagem Go usa o tipo de alias rune para dados do código UTF-8.

a := "Hello, 世界" 

Você pode usar a palavra-chave range em um loop for para indexar qualquer string em Go, mesmo uma string do UTF-8. Os loops for e o range serão abordados mais detalhadamente na série de tutoriais. Por ora, o importante é saber que podemos usar essas ferramentas para a contagem dos bytes de uma determinada string:

package main  import "fmt"  func main() {     a := "Hello, 世界"     for i, c := range a {         fmt.Printf("%d: %sn", i, string(c))     }     fmt.Println("length of 'Hello, 世界': ", len(a)) } 

No bloco de código acima, declaramos a variável a e atribuímos o valor Hello, 世界 a ela. O texto atribuído possui caracteres do UTF-8.

Na sequência, usamos um loop for padrão, bem como a palavra-chave range. Em Go, a palavra-chave range irá indexar através de uma string que retorna um caractere de cada vez, além do índice de bytes em que o caractere está na string.

Ao usar a função fmt.Printf, fornecemos uma string de formato %d: %sn. O %d é o verbo de impressão para um dígito (neste caso, um número inteiro) e o %s é o verbo de impressão para uma string. Então, fornecemos os valores de i, que é o índice atual do loop for e de c, que é o caractere atual no loop for.

Por fim, imprimimos toda a extensão da variável a, usando a função integrada len.

Anteriormente, mencionamos que um rune é um alias para o int32 e pode ser constituído por um a quatro bytes. O caractere usa três bytes para definir e o índice se move de acordo quando varia pela string UTF-8. É por esse motivo que o i não fica em sequência quando é impresso.

Output0: H 1: e 2: l 3: l 4: o 5: , 6: 7: 世 10: 界 length of 'Hello, 世界':  13 

Como pode ver, o comprimento é mais longo do que o número de vezes que ele levou para atravessar a string.

Nem sempre você vai trabalhar com strings do UTF-8, mas – quando tiver que trabalhar – já vai saber por que se tratam de runes e não de um int32 único.

Declarando tipos de dados para variáveis

Agora que você conhece os diferentes tipos de dados primitivos, vamos examinar como atribuir esses tipos às variáveis em Go.

Em Go, podemos definir uma variável com a palavra-chave var, seguida do nome da variável e do tipo de dados desejado.

No exemplo a seguir, vamos declarar uma variável chamada pi do tipo float64.

A palavra-chave var é a primeira coisa a ser declarada:

var pi float64 

Seguida pelo nome de nossa variável, pi:

var pi float64 

E, por fim, o tipo de dados float64:

var pi float64 

Opcionalmente, também podemos especificar um valor inicial, como 3.14:

var pi float64 = 3.14 

Go é uma linguagem do tipo estática. Ser do tipo estática significa que cada instrução no programa é verificada no momento da compilação. Significa, também, que o tipo de dados é ligado à variável, enquanto que em linguagens dinamicamente ligadas, o tipo de dados é ligado ao valor.

Por exemplo, em Go, o tipo é declarado ao se declarar uma variável:

var pi float64 = 3.14 var week int = 7 

Cada uma dessas variáveis poderia ser de um tipo diferente de dados, caso você as declarasse de outro modo.

É diferente do que ocorre com uma linguagem como a PHP, na qual o tipo de dados está associado ao valor:

$s = "sammy";         // $s is automatically a string $s = 123;             // $s is automatically an integer 

No bloco de código anterior, o primeiro $s é uma string porque lhe foi atribuído o valor "sammy" e o segundo é um número inteiro, pois tem o valor 123.

Em seguida, vamos examinar tipos de dados mais complexos, como as matrizes.

Matrizes

Uma matriz é uma sequência ordenada de elementos. A capacidade de uma matriz é definida na hora de sua criação. Assim que uma matriz tiver um tamanho alocado a ela, o tamanho já não poderá ser alterado. Como o tamanho de uma matriz é estático, isso significa dizer que a alocação de memória ocorre uma única vez. Isso torna as matrizes um pouco rígidas com que se trabalhar, mas aumenta o desempenho do seu programa. Por isso, as matrizes são tipicamente usadas na otimização de programas. As fatias, abordadas a seguir, são mais flexíveis e constituem o que você poderia considerar como matrizes em outras linguagens.

As matrizes são definidas pela declaração do tamanho da matriz e, em seguida, o tipo de dados com os valores definidos entre chaves { }.

Uma matriz de strings se parece com esta:

[3]string{"blue coral", "staghorn coral", "pillar coral"} 

Podemos armazenar uma matriz em uma variável e imprimi-la:

coral := [3]string{"blue coral", "staghorn coral", "pillar coral"} fmt.Println(coral) 
Output[blue coral staghorn coral pillar coral] 

Como mencionado anteriormente, as fatias se assemelham às matrizes, porém são bem mais flexíveis. Vamos dar uma olhada nesse tipo de dados mutável.

Fatias

Uma fatia é uma sequência ordenada de elementos cujo comprimento pode ser alterado. As fatias podem aumentar seu tamanho de maneira dinâmica. Se você adicionar novos itens a uma fatia e ela não tiver memória suficiente para armazenar os novos itens, ela solicitará mais memória do sistema, conforme necessário. Como a fatia pode ser expandida para adicionar mais elementos quando necessário, sua utilização é mais comum que a das matrizes.

As fatias são definidas pela declaração do tipo de dados, antecedida de um par de colchetes [] e tendo valores entre chaves { }.

Uma fatia de números inteiros se parece com esta:

[]int{-3, -2, -1, 0, 1, 2, 3} 

Uma fatia de floats se parece com esta:

[]float64{3.14, 9.23, 111.11, 312.12, 1.05} 

Uma fatia de strings se parece com esta:

[]string{"shark", "cuttlefish", "squid", "mantis shrimp"} 

Vamos definir nossa fatia de strings como seaCreatures:

seaCreatures := []string{"shark", "cuttlefish", "squid", "mantis shrimp"} 

Podemos imprimi-las chamando a variável:

fmt.Println(seaCreatures) 

O resultado será exatamente como a lista que criamos:

Output[shark cuttlefish squid mantis shrimp] 

Podemos usar a palavra-chave append para adicionar um item à nossa fatia. O comando a seguir adicionará o valor da string de seahorse à fatia:

seaCreatures = append(seaCreatures, "seahorse") 

Você pode verificar se ele foi adicionado, imprimindo a fatia:

fmt.Println(seaCreatures) 
Output[shark cuttlefish squid mantis shrimp seahorse] 

Como pode ver, caso precise gerenciar um tamanho desconhecido de elementos, uma fatia será muito mais versátil que uma matriz.

Mapas

O mapa é o tipo hash ou dicionário integrado da linguagem Go. Os mapas usam chaves e valores como um par para armazenar dados. Em programação, isso é útil quando precisamos fazer uma rápida busca de valores por meio de um índice ou, neste caso, de uma chave. Por exemplo, você pode querer manter um mapa de usuários, indexado pelos seus IDs de usuário. A chave seria o ID do usuário e o objeto do usuário seria o valor. Um mapa é construído pelo uso da palavra-chave map, seguida do tipo de dados-chave – entre colchetes [ ] – e, depois, pelo tipo de dados do valor e dos pares de valores-chave – entre chaves.

map[key]value{} 

Normalmente usado para reter dados relacionados, como a informação contida em um ID, um mapa se parece com isto:

map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"} 

Você verá que, além das chaves, há também sinais de dois pontos ao longo do mapa. As palavras à esquerda dos dois pontos são as chaves. As chaves podem ser de qualquer tipo comparável em Go. Tipos comparáveis são tipos primitivos como strings, ints etc. Um tipo primitivo é definido pela linguagem e não é compilado por meio da combinação de outros tipos. Embora possam ser tipos definidos pelo usuário, o recomendado como melhor prática é manter tudo simples para evitar erros de programação. As chaves no dicionário acima são: name, animal, color e location.

As palavras à direita dos dois pontos são os valores. Os valores podem constituir-se de qualquer tipo de dados. Os valores no dicionário acima são: Sammy, shark, blue e ocean.

Vamos armazenar o mapa dentro de uma variável e imprimi-lo:

sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"} fmt.Println(sammy) 
Outputmap[animal:shark color:blue location:ocean name:Sammy] 

Se quisermos isolar a cor da Sammy, podemos fazer isso chamando sammy["color"]. Vamos imprimir isso:

fmt.Println(sammy["color"]) 
Outputblue 

Como os mapas oferecem pares chave-valor para a armazenagem de dados, eles podem ser elementos importantes em seu programa Go.

Conclusão

Neste ponto, você já deverá ter uma melhor compreensão sobre alguns dos principais tipos de dados disponíveis para uso na linguagem Go. Cada um desses tipos de dados irá tornar-se importante, à medida que você desenvolver projetos de programação na linguagem Go.

Assim que você tiver um domínio sólido dos tipos de dados disponíveis em Go, você poderá aprender Como converter tipos de dados para alterar seus tipos de dados de acordo com a situação.