Entendendo Tipos de Dados no Go

Introdução

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

Neste artigo, abordaremos os tipos de dados importantes nativos do Go. Este não é um exame exaustivo dos tipos de dados, mas ajudará você a se familiarizar com as opções disponíveis no Go. Compreender alguns tipos de dados básicos permitirá que você escreva um código mais claro e que funcione de maneira eficiente.

Contexto

Uma maneira de pensar sobre 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 inteiros (0, 1, 2, …), inteiros (…, -1, 0, 1, …) e números irracionais (π), por exemplo.

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

5 + π

Podemos manter a equação como resposta para considerar o número irracional, ou arredondar π para um número com um número reduzido de casas decimais, e então somar os números:

5 + π = 5 + 3.14 = 8.14 

Mas, se começarmos a tentar avaliar 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. Como resultado, temos que ter cuidado com a maneira como usamos diferentes tipos de dados para atribuir valores e como os manipulamos através de operações.

Inteiros

Como na matemática, 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 um int. Assim como em outras linguagens de programação, você não deve usar vírgulas em números de quatro dígitos ou mais, então quando você escreve 1.000 no seu programa, escreva-o como 1000.

Podemos imprimir um inteiro de uma maneira 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, assim:

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

Podemos fazer matemática com inteiros em Go também. No seguinte bloco de código, usaremos o operador de atribuição := para declarar e instanciar a variável sum:

sum := 116 - 68
fmt.Println(sum)
Output
48

Como a saída mostra, o operador matemático - subtraiu o inteiro 68 de 116, resultando em 48. Você aprenderá mais sobre declaração de variáveis na seção Declarando Tipos de Dados para Variáveis.

Números inteiros podem ser utilizados de várias maneiras em programas Go. À medida que você continua aprendendo sobre Go, terá muitas oportunidades de trabalhar com inteiros e expandir seu conhecimento sobre esse tipo de dado.

Números de Ponto Flutuante

Um número de ponto flutuante ou um float é usado para representar números reais que não podem ser expressos como inteiros. Números reais incluem todos os números racionais e irracionais, e por isso, números de ponto flutuante podem conter uma parte fracionária, como 9.0 ou -116.42. Para fins de considerar um float em um programa Go, é um número que contém um ponto decimal.

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

fmt.Println(-459.67)
Output
-459.67

Também podemos declarar uma variável que representa um float, da seguinte forma:

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

Assim como com inteiros, podemos fazer operações matemáticas com floats em Go, também:

var sum = 564.0 + 365.24
fmt.Println(sum)
Output
929.24

Com inteiros e números de ponto flutuante, é importante lembrar que 3 ≠ 3.0, pois 3 refere-se a um inteiro enquanto 3.0 refere-se a um float.

Tamanhos dos Tipos Numéricos

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

A maioria das arquiteturas de sistemas hoje em dia são de 32 bits ou 64 bits. Por exemplo, você pode estar desenvolvendo para um laptop moderno com Windows, no qual o sistema operacional roda em uma arquitetura de 64 bits. No entanto, se você estiver desenvolvendo para um dispositivo como um relógio de fitness, pode estar trabalhando com uma arquitetura de 32 bits. Se você usar um tipo independente de arquitetura como int32, independentemente da arquitetura para a qual compilar, o tipo terá um tamanho constante.

O segundo tipo é um tipo específico de implementação. Neste tipo, o tamanho em bits pode variar com base na arquitetura em que o programa é construído. Por exemplo, se usarmos o tipo int, quando Go compila 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á 64 bits de tamanho.

Além de tipos de dados terem diferentes tamanhos, tipos como inteiros também vêm em dois tipos básicos: com sinal e sem sinal. Um int8 é um inteiro com sinal e pode ter um valor de -128 a 127. Um uint8 é um inteiro sem sinal e só pode ter um valor positivo de 0 a 255.

Os intervalos são baseados no tamanho em bits. Para dados binários, 8 bits podem representar um total de 256 valores diferentes. Como um tipo int precisa suportar valores positivos e negativos, um inteiro de 8 bits (int8) terá um intervalo de -128 a 127, totalizando 256 valores possíveis únicos.

Go possui os seguintes tipos 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)

Números de ponto flutuante e complexos também vêm em tamanhos variados:

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

Existem também alguns tipos numéricos de alias, que atribuem nomes úteis a tipos de dados específicos:

byte        alias for uint8
rune        alias for int32

O propósito do alias byte é deixar claro quando seu programa está usando bytes como uma medida comum na computação em elementos de string de caracteres, em oposição a pequenos inteiros não relacionados à medida de dados de bytes. Embora byte e uint8 sejam idênticos uma vez que o programa é compilado, byte é frequentemente usado para representar dados de caracteres na forma numérica, enquanto uint8 é destinado a ser um número em seu programa.

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

Além disso, Go possui 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 

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

Escolhendo Tipos de Dados Numéricos

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

Como discutido anteriormente neste artigo, existem tipos independentes de arquitetura e tipos específicos de implementação. Para dados inteiros, é comum em Go usar os tipos de implementação como int ou uint em vez de int64 ou uint64. Isso geralmente resultará na velocidade de processamento mais rápida para a arquitetura de destino. Por exemplo, se você usar um int64 e compilar para uma arquitetura de 32 bits, levará pelo menos duas vezes mais tempo para processar esses valores, pois leva ciclos de CPU adicionais para mover os dados pela arquitetura. Se você usasse um int em vez disso, o programa o definiria como um tamanho de 32 bits para uma arquitetura de 32 bits e seria significativamente mais rápido para processar.

Se você souber que não excederá um intervalo de tamanho específico, então escolher um tipo independente de arquitetura pode aumentar a velocidade e diminuir o uso de memória. Por exemplo, se você souber que seus dados não excederão o valor de 100 e serão apenas números positivos, então escolher um uint8 tornaria seu programa mais eficiente, pois exigiria menos memória.

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

Estouro vs. Wraparound

Go tem o potencial de tanto estourar um número quanto wraparound um número quando você tenta armazenar um valor maior do que o tipo de dados foi projetado para armazenar, dependendo se o valor é calculado em tempo de compilação ou em tempo de execução. Um erro em tempo de compilação ocorre quando o programa encontra um erro enquanto tenta construir o programa. Um erro em tempo de execução ocorre após a compilação do programa, enquanto ele está realmente executando.

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

package main

import "fmt"

func main() {
	var maxUint32 uint32 = 4294967295 // Tamanho máximo uint32
	fmt.Println(maxUint32)
}

Ele será compilado e executado com o seguinte resultado:

Output
4294967295

Se adicionarmos 1 ao valor em tempo de execução, ele wraparound para 0:

Output
0

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

package main

import "fmt"

func main() {
	var maxUint32 uint32 = 4294967295 + 1
	fmt.Println(maxUint32)

}

Em tempo de compilação, se o compilador puder determinar que um valor será muito grande para ser mantido no tipo de dados especificado, ele lançará um erro de overflow. Isso significa que o valor calculado é muito grande para o tipo de dados que você especificou.

Como o compilador pode determinar que vai estourar o valor, agora lançará um erro:

Output
prog.go:6:36: constant 4294967296 overflows uint32

Compreender os limites dos seus dados ajudará a evitar potenciais bugs no seu programa no futuro.

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

Booleanos

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

Os valores true e false sempre estarão com um “t” minúsculo e “f” minúsculo, respectivamente, pois são identificadores predefinidos em Go.

Muitas operações em matemática nos dão respostas que avaliam para verdadeiro ou falso:

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

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

myBool := 5 > 8

Podemos então imprimir o valor booleano com uma chamada à função fmt.Println():

fmt.Println(myBool)

Como 5 não é maior que 8, receberemos a seguinte saída:

Output
false

À medida que escreve mais programas em Go, você se familiarizará mais com o funcionamento dos booleanos e como diferentes funções e operações que resultam em 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 pode ser uma constante ou uma variável. As strings existem dentro de aspas simples ` ou aspas duplas " em Go e têm características diferentes dependendo de qual tipo de aspas você usa.

Se você usar aspas simples, estará criando um literal de string bruto. Se você usar aspas duplas, estará criando um literal de string interpretado.

Literais de String Bruto

Literais de string bruto são sequências de caracteres entre aspas simples, frequentemente chamadas de crases. Dentro das aspas, qualquer caractere aparecerá exatamente como está exibido entre as aspas simples, exceto pelo próprio caractere de aspas simples.

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

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

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

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

Output
Say "hello" to Go!\n

Literais de string brutos também podem ser usados 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)
Output
This string is on multiple lines within a single back quote on either side.

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

Literais de String Interpretados

Literais de string interpretados são sequências de caracteres entre aspas duplas, como em "bar". Dentro das aspas, qualquer caractere pode aparecer, exceto nova linha e aspas duplas não escapadas. Para mostrar aspas duplas em uma string interpretada, você pode usar a barra invertida como caractere de escape, assim:

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

Você quase sempre usará literais de string interpretados porque eles permitem caracteres de escape dentro deles. Para mais informações sobre como trabalhar com strings, confira Uma Introdução ao Trabalho com Strings em Go.

Strings com Caracteres UTF-8

UTF-8 é um esquema de codificação usado para codificar caracteres de largura variável em um a quatro bytes. Go suporta caracteres UTF-8 prontamente, sem necessidade de configurações especiais, bibliotecas ou pacotes. Caracteres romanos como a letra A podem ser representados por um valor ASCII como o número 65. No entanto, com caracteres especiais como um caractere internacional , seria necessário o UTF-8. Go utiliza o tipo alias rune para dados UTF-8.

a := "Hello, 世界"

Você pode usar a palavra-chave range em um loop for para indexar através de qualquer string em Go, mesmo uma string UTF-8. Loops for e range serão abordados com mais profundidade mais adiante na série; por enquanto, é importante saber que podemos usar isso para contar os bytes em uma string dada:

package main

import "fmt"

func main() {
	a := "Hello, 世界"
	for i, c := range a {
		fmt.Printf("%d: %s\n", 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 de Hello, 世界 a ela. O texto atribuído contém caracteres UTF-8.

Em seguida, utilizamos um loop for padrão, bem como a palavra-chave range. Em Go, a palavra-chave range indexará através de uma string retornando um caractere por vez, bem como o índice de bytes em que o caractere se encontra na string.

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

Finalmente, imprimimos o comprimento total da variável a usando a função interna len.

Anteriormente, mencionamos que um rune é um alias para int32 e pode ser composto por um a quatro bytes. O caractere ocupa três bytes para ser definido e o índice se move de acordo ao percorrer a string UTF-8. É por isso que i não é sequencial quando é impresso.

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

Como você pode ver, o comprimento é maior do que o número de vezes que levou para percorrer a string.

Você não estará sempre trabalhando com strings UTF-8, mas quando estiver, agora entenderá por que elas são runes e não um único int32.

Declarando Tipos de Dados para Variáveis

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

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

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

A palavra-chave var é a primeira coisa declarada:

var pi float64

Seguido pelo nome da nossa variável, pi:

var pi float64

E, finalmente, o tipo de dado float64:

var pi float64

Podemos opcionalmente especificar um valor inicial também, como 3.14:

var pi float64 = 3.14

Go é uma linguagem estaticamente tipada. Estaticamente tipada significa que cada instrução no programa é verificada em tempo de compilação. Isso também significa que o tipo de dado está ligado à variável, enquanto em linguagens dinamicamente tipadas, o tipo de dado está ligado ao valor.

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

var pi float64 = 3.14
var week int = 7

Cada uma dessas variáveis poderia ser de um tipo de dado diferente se você as declarasse de maneira diferente.

Isso é diferente de uma linguagem como PHP, onde o tipo de dado está associado ao valor:

$s = "sammy";         // $s é automaticamente uma string
$s = 123;             // $s é automaticamente um inteiro

No bloco de código anterior, o primeiro $s é uma string porque recebe o valor "sammy", e o segundo é um inteiro porque tem o valor 123.

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

Arrays

Um array é uma sequência ordenada de elementos. A capacidade de um array é definida no momento da criação. Uma vez que um array tenha alocado seu tamanho, o tamanho não pode mais ser alterado. Como o tamanho de um array é estático, isso significa que ele só aloca memória uma vez. Isso torna os arrays um tanto rígidos de trabalhar, mas aumenta o desempenho do seu programa. Por essa razão, os arrays são tipicamente usados ao otimizar programas. Slices, abordados a seguir, são mais flexíveis e constituem o que você pensaria como arrays em outras linguagens.

Arrays são definidos ao declarar o tamanho do array, seguido pelo tipo de dado com os valores definidos entre chaves { }.

Um array de strings se parece com isto:

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

Podemos armazenar um array em uma variável e imprimi-lo:

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

Como mencionado anteriormente, slices são semelhantes a arrays, mas são muito mais flexíveis. Vamos dar uma olhada nesse tipo de dado mutável.

Slices

Um slice é uma sequência ordenada de elementos que pode mudar de comprimento. Slices podem aumentar seu tamanho dinamicamente. Quando você adiciona novos itens a um slice, se o slice não tiver memória suficiente para armazenar os novos itens, ele solicitará mais memória do sistema conforme necessário. Como um slice pode ser expandido para adicionar mais elementos quando necessário, eles são mais comumente usados do que arrays.

Fatias são definidas declarando o tipo de dado precedido por um colchete de abertura e fechamento [] e tendo valores entre chaves { }.

Uma fatia de inteiros parece com isto:

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

Uma fatia de floats parece com isto:

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

Uma fatia de strings parece com isto:

[]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)

A saída parecerá 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 seguinte comando adicionará o valor da string seahorse à fatia:

seaCreatures = append(seaCreatures, "seahorse")

Você pode verificar se foi adicionado imprimindo-o:

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

Como você pode ver, se precisar gerenciar um tamanho desconhecido de elementos, uma fatia será muito mais versátil do que uma matriz.

Mapas

O mapa é o tipo hash ou dicionário integrado do Go. Mapas usam chaves e valores como um par para armazenar dados. Isso é útil na programação para procurar valores rapidamente por um índice, ou neste caso, uma chave. Por exemplo, você pode querer manter um mapa de usuários, indexado por sua ID de usuário. A chave seria a ID de usuário, e o objeto de usuário seria o valor. Um mapa é construído usando a palavra-chave map seguida pelo tipo de dado da chave entre colchetes [ ], seguido pelo tipo de dado do valor e os pares chave-valor entre chaves.

map[key]value{}

Normalmente utilizado para armazenar dados relacionados, como as informações contidas em um ID, um mapa se parece com isto:

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

Você notará que, além das chaves, também existem dois pontos ao longo do mapa. As palavras à esquerda dos dois pontos são as chaves. As chaves podem ser 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 é construído a partir da combinação de outros tipos. Embora possam ser tipos definidos pelo usuário, é considerado uma prática recomendada mantê-los 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 ser compostos por qualquer tipo de dado. 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)
Output
map[animal:shark color:blue location:ocean name:Sammy]

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

fmt.Println(sammy["color"])
Output
blue

Como os mapas oferecem pares chave-valor para armazenar dados, eles podem ser elementos importantes no seu programa Go.

Conclusão

Neste momento, você deve ter uma compreensão melhor de alguns dos principais tipos de dados que estão disponíveis para você usar em Go. Cada um desses tipos de dados se tornará importante à medida que você desenvolver projetos de programação na linguagem Go.

Depois de ter uma compreensão sólida dos tipos de dados disponíveis para você em Go, você pode aprender Como Converter Tipos de Dados para alterar seus tipos de dados conforme a situação.

Source:
https://www.digitalocean.com/community/tutorials/understanding-data-types-in-go