Golang – 02. Tipos Básicos

Este artigo é a parte 2 de 11 na série Golang

Go é uma linguagem tipada, e, por isso, suas variáveis devem ser declaradas antes de serem usadas. Neste artigo, serão tratados constantes e os tipos básicos do Go, como inteiros, ponto flutuante, números complexos, string e booleanos, além de dar atenção a algumas particularidades da linguagem.

Tipos Numéricos

Go possui uma gama abrangente de tipos numéricos que vão de inteiros de 8 a 64 bits, com e sem sinal, ponto flutuante de 32 e 64 bits, além de números complexos, já em sua biblioteca padrão. Estes tipos numéricos estão apresentados na tabela a seguir:

DeclaraçãoDescriçãoIntervalo
uint8Inteiros positivos de 8-bits0 a 255
uint16Inteiros positivos de 16-bits0 a 65535
uint32Inteiros positivos de 32-bits0 a 4294967295
uint64Inteiros positivos de 64-bits0 a 18446744073709551615
int8Inteiros de 8-bits-128 a 127
int16Inteiros de 16-bits-32768 a 32767
int32Inteiros de 32-bits-2147483648 a 2147483647
int64Inteiros de 64-bits-9223372036854775808 a 9223372036854775807
float32Ponto flutuante 32-bitsaté 3,4028234663852886e+38
float64Ponto flutuante 64-bitsaté 1,7976931348623157e+308
complex64Complexo com float 32-bits para representar as partes real e imagináriaaté (1+i)*3,4028234663852886e+38
complex128Complexo com float 64-bits para representar as parte real e imagináriaaté (1+i)*1,7976931348623157e+308
byteAlias para uint80 a 255
runeAlias para uint320 a 65535

Byte é muitas vezes usado para representar caracteres através de seus códigos da tabela ASCII, enquanto rune é usado para representar caracteres Unicode que estende a tabela ASCII para alcançar todos os alfabetos possíveis e uma gama de caracteres e símbolos especiais. Observe que byte e rune são alias (“sinônimos”) para uint8 e uint32, respectivamente. Isso terá algumas implicações na conversão de inteiros em strings, como será mostrado adiante.

Declaração de Constantes

É comum em programas mapear alguns valores imutáveis categorizando-os como constantes ao longo de seu código. O Go possui uma palavra-chave para isso, const, que funciona de forma semelhante a de outras linguagens, veja o exemplo a seguir:

const pi = 3.1415

const (
idKey = "ID"
maxSize = 258
maxPress float32 = 8.98e5
enableKey = true
)

Constantes não necessitam ser tipadas, no entanto, constantes não tipadas são mais flexíveis e podem ser empregadas para iniciar diversas outros tipos de variáveis, como ilustra o código a seguir:

package main

import "fmt"

func main() {
	const x = 10

	var y int = x
	var z float32 = x
	var w byte = x

	fmt.Println(x, y, z, w)
}

Este código irá executar sem problemas. Mas se trocar a declaração da constante na linha 6 para uma declaração tipada como inteiros, por exemplo, isto acarretará um erro nas linhas 9 e 10: “cannot use x (constant 10 of type int) as type float32/byte in variable declaration“.

Constantes com iota

O Go possui a palavra-chave iota para gerar constantes sequenciais, como 0, 1, 2, …

Quando declarado dentro de um bloco const, a primeira ocorrência de iota é o inteiro zero e as subsequentes assumem os valores 1, 2, 3, … Isto é bem útil para declarar constantes sequenciais.

Por exemplo, o código a seguir:

...
const (
	primeiro = 0
	segundo  = 1
	terceiro = 2
	quarto   = 3
)
...

com a palavra chave iota fica assim

...
const (
	primeiro = iota
	segundo  = iota
	terceiro = iota
	quarto   = iota
)
...

ou simplesmente

...
const (
	primeiro = iota
	segundo
	terceiro
	quarto
)
...

O iota necessita ser invocado apenas uma vez.

O iota também pode ser usado em uma expressão para gerar valores diferenciados, como ilustrado no exemplo abaixo:

package main

import (
	"fmt"
)

type binario byte

const (
	_ = 1 << (10 * iota)
	KiB
	MiB
	GiB
	TiB
	PiB
	EiB
)

const (
	domingo = iota + 1
	segunda
	terca
	quarta
	quinta
	sexta
	sabado
)

const (
	micro = 3*iota - 6
	mili
	_
	quilo
	mega
	giga
)

func main() {
	fmt.Println("Potências Binárias:")
	fmt.Println("KiB: ", KiB)
	fmt.Println("MiB: ", MiB)
	fmt.Println("GiB: ", GiB)
	fmt.Println("TiB: ", TiB)
	fmt.Println("PiB: ", PiB)
	fmt.Println("EiB: ", EiB)

	fmt.Println("\nDias da semana:")
	fmt.Println("D: ", domingo)
	fmt.Println("S: ", segunda)
	fmt.Println("S: ", terca)
	fmt.Println("S: ", quarta)
	fmt.Println("S: ", quinta)
	fmt.Println("S: ", sexta)
	fmt.Println("S: ", sabado)

	fmt.Println("\nPrefixos:")
	fmt.Println("u: 10^", micro)
	fmt.Println("m: 10^", mili)
	fmt.Println("K: 10^", quilo)
	fmt.Println("M: 10^", mega)
	fmt.Println("G: 10^", giga)
}

A cada nova palavra-chave const, o iota é reiniciado para zero. O underline, “_”, é uma forma de descartar o valor passado e será muito empregado para descartar retorno de funções em artigos seguintes.

Declaração de Variáveis

O Go possui duas formas básicas para se declarar uma variável. Sendo a forma extensa declarada pela palavra-chave var e uma curta pelo operador “:=”. Independente da forma de declaração empregada em seu código, uma variável somente poderá ser declarada uma vez, num mesmo escopo.

Na forma extensa, usa-se a palavra-chave var com a sintaxe:

var i int64 
i = 10

sem iniciar a variável ou

var i int64 = 10

iniciando a variável. Ambos os códigos acima fazem o mesmo serviço, iniciando uma variável i como um inteiro 64-bits e passando o valor 10 para ele. Nesta mesma forma, pode-se iniciar várias variáveis de uma só vez com a sintaxe:

var i, j int64 = 10, 20

Inicia i e j como inteiros 64-bits e passa os valores 10 e 20, respectivamente. Iniciar diversos tipos num mesmo var pode ser feito assim:

var (
    i, j int64  = 10, 20
    x float64   = 2.5
    z complex64 = 6.8 + 4.9i
)

Experimente o código a seguir:

package main

import (
	"fmt"
)

func main() {
	var (
		i, j int64     = 10, 20
		x    float64   = 2.5
		z    complex64 = 6.8 + 4.9i
	)
	fmt.Printf("i: %v  tipo: %T\n", i, i)
	fmt.Printf("j: %v  tipo: %T\n", j, j)
	fmt.Printf("x: %v  tipo: %T\n", x, x)
	fmt.Printf("z: %v  tipo: %T\n", z, z)
}

Os métodos fmt.Printf(), empregados nas linhas 13 à 16, são semelhantes à função printf() do C e, por isso, vou deixar esta discussão para outro momento. Ao executar este código, deve retornar:

Bash

alves@arabel:~$ go run "numericos.go"
i: 10 tipo: int64
j: 20 tipo: int64
x: 2.5 tipo: float64
z: (6.8+4.9i) tipo: complex64

Uma declaração curta pode ser feita com o operador “:=”, sem a palavra chave var:

i := 10
x, y, z := 12.5, 34, 3+4i

Com a declaração curta, o tipo das variáveis é declarado por inferência em tempo de compilação, ou seja, no exemplo acima, i e y serão declarados como inteiros, x como float e z como complexo (32-bits/64-bits depende da arquitetura ques esteja sendo compilado). Desta forma, todo o comando var, linhas 8 à 12 do código anterior, pode ser substituído por uma linha com uma declaração curta:

package main

import (
	"fmt"
)

func main() {
	i, j, x, z := 10, 20, 2.5, 6.8+4.9i
	fmt.Printf("i: %v  tipo: %T\n", i, i)
	fmt.Printf("j: %v  tipo: %T\n", j, j)
	fmt.Printf("x: %v  tipo: %T\n", x, x)
	fmt.Printf("z: %v  tipo: %T\n", z, z)
}

O operador “:=” declara as variáveis e associa os valores à direita ao correspondente à esquerda. O operador “:=” é bastante conveniente, mas não pode ser confundido com o operador de atribuição “=”, que somente pode ser empregado em uma variável já declarada.

Como exemplo, segue o código simples para resolver uma equação de segundo grau pela equação de Bhaskara. Para este código, crie um diretório “segrau” e dentro dele o arquivo “segrau.go”, com o código a seguir:

package main

import (
	"fmt"
	"math"
)

func main() {
	var a, b, c float64 = 0., 0., 0.
	x1, x2 := 0., 0.

	fmt.Println("Resolve uma equação de 2o grau")
	fmt.Println("ax² + bx + c = 0")
	fmt.Print("\nEntre a, b, c: ")
	fmt.Scan(&a)
	fmt.Scan(&b)
	fmt.Scan(&c)

	fmt.Printf("%.2fx²+%.2fx + %.2f = 0.00\n", a, b, c)
	// declarando e associando valores
	d := math.Pow(b, 2) - 4*a*c
	a2, sqrd := 2*a, math.Sqrt(d)

	// como x1 e x2 foram iniciados na linha 10, deve-se usar o operados = para 
	// novas atribuições de valores
	x1, x2 = (-b+sqrd)/a2, (-b-sqrd)/a2

	fmt.Printf("x1: %.2f  x2: %.2f\n", x1, x2)
	fmt.Println("Teste x1:", a*math.Pow(x1, 2)+b*x1+c)
	fmt.Println("Teste x2:", a*math.Pow(x2, 2)+b*x2+c)
}

Como no programa do Golang – 01. Introdução, o método fmt.Scan(&var) é empregado para carregar as três variáveis a, b e c. Observe que o buffer do teclado é lido três vezes seguidas, linhas 14, 15 e 16, com isso é possível passar os três valores simultaneamente, separados por vírgula, como “3,5,-3”, sem as aspas.

As linhas 21, 22, 29 e 30 usam o pacote math para acessar as funções matemáticas de raiz quadrada, Sqrt(), e potência, Pow(). Observe que o var e o operador := são empregados apenas na primeira ocorrência de qualquer variável, ou seja, em sua declaração. Associações posteriores de novos valores somente podem ocorrer com o emprego do operador de associação “=“, como ocorre na linha 26 para as variáveis x e y.

Execute este código no terminal com o comando a seguir:

Bash

alves@segrau:~$ go run "segrau.go"
Resolve uma equação de 2o grau
ax² + bx + c = 0

Entre a, b, c: 2,5,-3
2.00x²+5.00x + -3.00 = 0.00
x1: 0.50 x2: -3.00

Teste x1: 0
Teste x2: 0
alves@segrau:~$ go run segrau.go

Resolve uma equação de 2o grau
ax² + bx + c = 0


Entre a, b, c:
2,5,3
2.00x²+5.00x + 3.00 = 0.00
x1: -1.00 x2: -1.50
Teste x1: 0
Teste x2: 0

As duas últimas linhas do programa testam as raízes no polinômio de segundo grau correspondente, para atestar que estas estejam corretas, retornando 0 como resultado em caso afirmativo. Este código é limitado e não resolve raízes complexas, por isso pode gerar erro dependendo do polinômio passado.

Declaração via “var” ou “:=”

Existe uma pequena diferença entre a declaração curta, :=, e uma declaração regular, via a palavra reservada var: Numa declaração curta, é possível redeclarar variáveis no mesmo bloco, desde que uma das variáveis declaradas seja uma nova declaração. Neste caso, a “redeclaração” não cria uma nova variável, apenas atribui um novo valor à variável original.

O código a seguir ilustra a “redeclaração” da variável c:

package main

import (
	"fmt"
)

func main() {
	var c = 0

	a, c := 1, 2
	fmt.Println(a, c)

	b, c := 3, 4
	fmt.Println(a, b, c)

	a, b, c, d := 5, 6, 7, 8
	fmt.Println(a, b, c, d)
}

A linha 8 pode ser omitida, e o resultado deste código será o mesmo. Adicionei-a apenas para mostrar que a variável pode ter sido declarada com um var e ainda assim pode ser “redeclarada” em linhas posteriores com uma declaração curta seguida de mais uma nova variável.

Observe que a variável c é “redeclarada” nas linhas 10, 14 e 16. Nesta última linha, as variáveis a e b também são “redeclaradas”. No entanto, em cada linha, uma nova variável é declarada. Este código imprimirá:

1 2
1 3 4
5 6 7 8

Uma “redeclaração” das variáveis de fato não ocorre no Go; o que vemos aqui é apenas um novo valor sendo associado às variáveis já declaradas, seguido da declaração de uma nova variável. Se tentar mudar o tipo de qualquer uma das variáveis já declaradas, isso gerará um erro. Se testar o código com a mudança a seguir:

package main

import (
	"fmt"
)

func main() {
	var c = 0

	a, c := 1, 2
	fmt.Println(a, c)

	b, c := 3, 4
	fmt.Println(a, b, c)

	a, b, c, d := "5.3", 6, 7, 8
	fmt.Println(a, b, c, d)
}

Irá gerar o erro:

Bash

alves@arabel:var$ go run “/home/rudson/go/src/rudsonalves/Golang/Aula 01/var/var.go”
# command-line-arguments
src/rudsonalves/Golang/Aula 01/var/var.go:68:16: cannot use “5.3” (untyped
string constant) as int value in assignment

No entanto, esta técnica de “redeclaração” será muito útil adiante, principalmente na coleta de erros em funções.

Arrays

Arrays são listas de variáveis do mesmo tipo organizadas em forma sequencial. Um array de inteiros pode ser definido nas seguintes formas:

var v [4]int

Isso cria uma array de 4 inteiros, todos iniciados como 0 (variáveis numéricas, quando declaradas sem um valor definido, são sempre iniciadas como zeros).

var z = [3]int{2, 4, 6}

Isso cria um array com os três inteiros 2, 4 e 6. Também é possível iniciar valores em posições específicas de um array, como no exemplo a seguir:

var w = [10]int{1, 1, 5:4, 8:77}

O “5:4” posiciona o elemento de índice 5 com o valor 4, da mesma forma que o “8:77” adiciona um 77 na posição de índice 8 do array. O array resultante ficará com os elementos [1, 1, 0, 0, 0, 4, 0, 0, 77, 0]. Essas mesmas inicializações podem ser feitas de forma curta com o operador “:=”, como está apresentado no código a seguir:

package main

import "fmt"

func main() {
	v := [4]int{}
	z := [3]int{2, 4, 6}
	w := [10]int{1, 1, 5: 4, 8: 7}
	m := [3][2]int{{1, 2}, {3, 4}, {5, 6}}

	fmt.Println("v: ", v)
	fmt.Println("z: ", z)
	fmt.Println("w: ", w)
	fmt.Println("m: ", m)
}

Na linha 9, foi definida uma matriz de 3×2 elementos inteiros. A execução deste código retornará:

v: [0 0 0 0]
z: [2 4 6]
w: [1 1 0 0 0 4 0 0 7 0]
m: [[1 2] [3 4] [5 6]]

Arrays possuem um comprimento fixo, e seu valor pode ser acessado pela função len(), e os elementos de um array são acessados passando seu índice entre colchetes, como é comum em outras linguagens.

package main

import "fmt"

func main() {
	v := [4]int{}
	z := [3]int{2, 4, 6}
	w := [10]int{1, 1, 5: 4, 8: 7}
	m := [3][2]int{{1, 2}, {3, 4}, {5, 6}}

	v[0] = z[0] + w[8]    // 2 + 7 = 9
	v[1] = w[1] + m[2][1] // 1 + 6 = 7
	v[2] = m[1][1]        // 4
	v[3] = 12

	fmt.Println("v:", v, "Comprimento:", len(v))
}

Com essas mudanças, o programa imprimirá “v: [9 7 4 12] Comprimento: 4“.

Slices

Assim como arrays, os slices são listas de variáveis do mesmo tipo, mas com o comprimento incremental, podendo crescer ao longo do código. Sua declaração é semelhante à de um array, mas sem declarar o número de elementos.

var x = []int{1,3,5,7}

Ou na forma curta:

y := []int{2,4,6}

Uma diferença importante entre arrays e slices é que os elementos de um slice podem ser nulos, como na declaração a seguir:

var z []int

Esta linha declara um slice de comprimento nulo e sem elementos. Se testado, este slice é nil. Este mesmo slice também pode ser declarado como:

w := []int{}

Com uma pequena diferença, neste caso, o slice criado será [] e não nil. O código abaixo demonstra essa diferença:

package main

import "fmt"

func main() {
	var x = []int{1, 3, 5, 7}
	y := []int{2, 4, 6}
	var z []int
    w := []int{}

	fmt.Printf("x: %v  Len: %v\n", x, len(x))
	fmt.Printf("y: %v  Len: %v\n", y, len(y))
	fmt.Printf("z: %v  Len: %v\n", z, len(z))
  
	fmt.Println("z == nil: ", z == nil)
	fmt.Println("w == nil: ", w == nil)
}

O retorno deste código será:

x: [1 3 5 7]  Len: 4
y: [2 4 6]  Len: 3
z: []  Len: 0
w: []  Len: 0
z == nil:  true
w == nil:  false

Slices são o primeiro tipo que não pode ser comparado entre si. Embora z e w aparentem ser idênticos, uma tentativa de compará-los gera o erro: “invalid operation: w == z (slice can only be compared to nil)“. Portanto, slices somente podem ser comparados a nil.

Função Append

Slices podem ser acrescidos de novos elementos por meio da função append(), que adiciona novos elementos ao final do slice. Sua sintaxe é apresentada a seguir:

var1 = append(var1,i,j...)

Onde i, j... são novos elementos do tipo de var1, a serem adicionados ao final de var1. Observe que a função append criará um novo slice com o conteúdo de var1, seguido de i, j, .... Por fim, este é associado ao slice var1, à esquerda da expressão.

É possível fazer um append de um slice em outro, no entanto, isso equivale a fazer com que as duas variáveis apontem para o mesmo slice. O código a seguir ilustra isso:

package main

import "fmt"

func main() {
	x := []int{1, 2, 3}

	y := append(x, 44, 55)

	var z = []int{}
	z = append(x)
	z[0] = 11
	fmt.Println("x:", x, " y:", y, " z:", z)
}

Sua saída será:

x: [11 2 3]  y: [1 2 3 44 55]  z: [11 2 3]

Observe que x e z são o mesmo slice. A linha 11 pode gera um Warning no VS Code. Embora a linha não esteja errada ela equivale a escrever z = x.

Função Capacidade e Comprimento

Slices foram concebidas para serem listas de acesso rápido e, por isso, são alocadas em uma cadeia contínua de memória, o que torna mais rápido o acesso a seus elementos.

Para melhor gerenciar a alocação dos slices, além do atributo comprimento (medido pela mesma função len() usada em arrays), um slice possui o atributo capacidade, que indica a máxima quantidade de elementos que estão reservados na memória para a adição de novos elementos. Sua capacidade é lida através da função cap().

O código a seguir ilustra a relação entre comprimento e capacidade, bem como o aumento da capacidade com a adição de novos elementos:

package main

import "fmt"

func main() {
	x := []int{}
	y := []int{0}
	z := []int{0, 1, 3}

	fmt.Println("x:", x, " Len(x):", len(x), " Cap(x):", cap(x))
	fmt.Println("y:", y, " Len(y):", len(y), " Cap(y):", cap(y))
	fmt.Println("z:", z, " Len(z):", len(z), " Cap(z):", cap(z))

	fmt.Println("")
	x = append(x, 1)
	y = append(y, 2)
	z = append(z, 4, 5)

	fmt.Println("x:", x, " Len(x):", len(x), " Cap(x):", cap(x))
	fmt.Println("y:", y, " Len(y):", len(y), " Cap(y):", cap(y))
	fmt.Println("z:", z, " Len(z):", len(z), " Cap(z):", cap(z))
}

Este código gera a saída:

x: []  Len(x): 0  Cap(x): 0
y: [0]  Len(y): 1  Cap(y): 1
z: [0 1 3]  Len(z): 3  Cap(z): 3

x: [1]  Len(x): 1  Cap(x): 1
y: [0 2]  Len(y): 2  Cap(y): 2
z: [0 1 3 4 5]  Len(z): 5  Cap(z): 6

Como pode observar, no momento da criação, o slice possui a capacidade de armazenamento igual ao seu comprimento. Quando novos elementos são adicionados, seu comprimento cresce, mas quando seu comprimento supera sua capacidade, o slice é realocado para uma nova posição na memória, onde caiba um slice do dobro do tamanho do slice original. Este processo se repete, para incrementos de um elemento, até a capacidade de 512. Após isso, sua capacidade é aumentada em incrementos menores que o dobro do comprimento.

Função Make

A função make permite criar slices vazios com capacidade especificada. Sua sintaxe é:

x := make([]int, comprimento[, capacidade])

A capacidade é opcional e, se ausente, a capacidade é igual ao comprimento. Veja exemplos no código a seguir:

package main

import "fmt"

func main() {
	x := make([]int, 3, 10)
	var y = make([]float32, 0, 12)

	fmt.Println("x:", x, " len(x):", len(x), " cap(x):", cap(x))
	fmt.Println("y:", y, " len(y):", len(y), " cap(y):", cap(y))
}

A declaração da linha 6 cria o slice x = [0, 0, 0], com capacidade de 10 elementos e, na linha 7, o slice y de float32 vazio, y = [], de capacidade de 12 elementos.

Slicing Slices

A primeira vez que me deparei com o conceito de slice foi em Python, onde ele representava exatamente o que sua tradução livre sugere: “fatiar”. Em Go, é possível criar uma fatia de um slice da mesma maneira que em Python.

A sintaxe para criar uma fatia de um slice ou até mesmo de um array é a seguinte:

y := x[i1:i2]

Essa linha irá criar uma nova fatia chamada y que conterá os elementos de x no intervalo de índices de i1 até i2-1. Os índices i1 e i2 podem ser omitidos ao criar a fatia:

  • Se omitirmos o índice i1 (x[:i2]), obteremos uma fatia que começa no primeiro elemento de x até o elemento de índice i2-1.
  • Se omitirmos o índice i2 (x[i1:]), a fatia incluirá todos os elementos de x a partir do elemento de índice i1 até o último elemento.

Por exemplo, se x for um slice contendo os números pares de 2 a 12, as fatias de x abaixo:

x := []int{2,4,6,8,10,12} 
y := x[2:5] 
z := x[:3] 
w := x[4:] 
k := x[2:3]

produzirão os seguintes resultados: y = [6 8 10], z = [2 4 6], w = [10 12] e k = [6].

Uma fatia é essencialmente um “ponteiro” para os elementos do slice original. Portanto, se você modificar os elementos compartilhados, essas alterações serão visíveis em todas as fatias que compartilham o elemento modificado.

O código a seguir ilustra a criação de fatias de um slice e como as alterações se refletem:

package main

import "fmt"

func main() {
	x := []int{2, 4, 6, 8, 10, 12}
	y := x[2:5]
	z := x[:3]
	w := x[4:]
	k := x[2:3]

	fmt.Println("x:", x)
	fmt.Println("y:", y)
	fmt.Println("z:", z)
	fmt.Println("w:", w)
	fmt.Println("k:", k)

	fmt.Println("\nTrocando elementos")
	y[0] = 122
	fmt.Println("x:", x)
	fmt.Println("y:", y)
	fmt.Println("z:", z)
	fmt.Println("w:", w)
	fmt.Println("k:", k)
}

Sua saída será:

x: [2 4 6 8 10 12]
y: [6 8 10]
z: [2 4 6]
w: [10 12]
k: [6]

Trocando elementos
x: [2 4 122 8 10 12]
y: [122 8 10]
z: [2 4 122]
w: [10 12]
k: [122]

Como você pode observar, todas as modificações nas fatias de x afetam os elementos compartilhados.

Superposição inesperadas em Slices com Append

Superposições inesperadas podem ocorrer quando se utiliza o append() em fatias de um slice. Isso acontece porque, em muitos casos, uma fatia de um slice é tratada como um “ponteiro” para parte do slice original. Quando aplicamos o append a essas fatias, estamos sobrescrevendo o slice original, o que pode gerar resultados inesperados. Veja o exemplo no código a seguir:

package main

import "fmt"

func main() {
	x := []int{1, 2, 3, 4, 5, 6}
	y := x[:2]
	z := x[3:]

	fmt.Println("a. x:", x, "y:", y, "z:", z)

	fmt.Println("\nb. Append [30, 40] a y")
	y = append(y, 30, 40)
	fmt.Println("c. x:", x, "y:", y, "z:", z)

	fmt.Println("\nd. Append [70] a z")
	z = append(z, 70)
	fmt.Println("e. x:", x, "y:", y, "z:", z)

	fmt.Println("\nf. Append [77] a a")
	x = append(x, 77)
	fmt.Println("g. x:", x, "y:", y, "z:", z)

	fmt.Println("\nh. Append [80] a z")
	z = append(z, 80)
	fmt.Println("i. x:", x, "y:", y, "z:", z)

	fmt.Println("\nj. z[1] = 50")
	z[1] = 50
	fmt.Println("k. x:", x, "y:", y, "z:", z)
}

A linha 6 cria um slice x com 6 elementos e conteúdo [1, 2, 3, 4, 5, 6]. A linha 7 cria um slice que é um apontador para os dois elementos iniciais do slice x (y = [1, 2]), e a linha 8 cria o slice z que aponta para os três últimos elementos do slice x (z = [4, 5, 6]).

Os problemas iniciam nas próximas linhas com a aplicação dos comandos append(). Na linha 13 o slice y é acrescido com os elementos [30, 40]. Isto aumenta o comprimento do slice y e sobrepõe o slice x e também o primeiro elemento de z. Veja a saída nas linhas b e c:

a. x: [1 2 3 4 5 6] y: [1 2] z: [4 5 6]

b. Append [30, 40] a y
c. x: [1 2 30 40 5 6] y: [1 2 30 40] z: [40 5 6]

d. Append [70] a z
e. x: [1 2 30 40 5 6] y: [1 2 30 40] z: [40 5 6 70]

f. Append [77] a a
g. x: [1 2 30 40 5 6 77] y: [1 2 30 40] z: [40 5 6 70]

h. Append [80] a z
i. x: [1 2 30 40 5 6 77] y: [1 2 30 40] z: [40 5 6 70 80]

j. z[1] = 50
k. x: [1 2 30 40 5 6 77] y: [1 2 30 40] z: [40 50 6 70 80]

No entanto, ao adicionar [70] ao slice z, linha 17, a associação entre x e z é rompida e z passa a ser um slice independente. Isto fica claro na linha 21, onde é adicionado um 77 ao slice x e este não sobrepõe o valor de z, linhas f e g acima. Para fechar é adicionado um 80 a z e o seu elemento de índice 1 é alterado para 50, o que não interfere em y e z.

O importante aqui é você ter certeza de que o comportamento apresentado é de fato o que o seu código espera para não ter um resultado inesperado.

Se quiser deixar as cópias independentes é necessário adicionar mais um parâmetro ao slicing, apontando o último elemento a ser copiado do slice principal. A alteração das linhas 7 e 8 para:

y := x[:2:2]
z := x[3:5:5]

faz com que os slicing atribuídos a y e z sejam cópias independentes, alterando a saída do programa anterior para:

a. x: [1 2 3 4 5 6] y: [1 2] z: [4 5]

b. Append [30, 40] a y
c. x: [1 2 3 4 5 6] y: [1 2 30 40] z: [4 5]

d. Append [70] a z
e. x: [1 2 3 4 5 6] y: [1 2 30 40] z: [4 5 70]

f. Append [77] a a
g. x: [1 2 3 4 5 6 77] y: [1 2 30 40] z: [4 5 70]

h. Append [80] a z
i. x: [1 2 3 4 5 6 77] y: [1 2 30 40] z: [4 5 70 80]

j. z[1] = 50
k. x: [1 2 3 4 5 6 77] y: [1 2 30 40] z: [4 50 70 80]

Neste momento as alterações em y e z não afetam x.

Função Copy

Uma outra maneira de criar uma cópia de um slice é através da função copy(). Em poucas linhas de código, a função copy cria uma cópia independente de um slice e retorna o número de elementos copiados.

No entanto, o número de elementos copiados depende do tamanho do slice de destino. Se o slice de destino for menor do que o slice de origem, apenas os primeiros elementos serão copiados. Portanto, para realizar as cópias anteriores, é necessário escrever um pouco mais de código para definir os tamanhos corretos dos slices de destino para acomodar as cópias.

Você pode experimentar a seguinte modificação no início do programa anterior:

package main

import "fmt"

func main() {
	x := []int{1, 2, 3, 4, 5, 6}
	y := make([]int, 2)
	z := make([]int, 3)
	copy(y, x[:2])
	copy(z, x[3:])

	fmt.Println("a. x:", x, "y:", y, "z:", z)
	...

As declarações nas linhas 7 e 8 inicializam y e z como slices de inteiros com comprimentos 2 e 3, respectivamente. As linhas 9 e 10 permitem que as cópias independentes dos slices x[:2] e x[3:] sejam transferidas para y e z, preenchendo todos os seus elementos. O resultado dessa alteração será o mesmo que na alteração anterior, criando slices independentes, mas agora usando a função copy().

Strings

Strings são sequências de caracteres e, portanto, também são slices de caracteres.

var a string = "Imaginação"
b := a[3:6]

A segunda linha cria um slice de a chamado b (“gin“). Strings podem ser representadas entre crases quando se deseja incluir textos longos com várias linhas ou para escapar aspas simples e duplas. As aspas simples são usadas para representar um único Unicode, como na função rune().

package main

import (
	"fmt"
)

func main() {
	frase := `"Duas coisas são infinitas: o universo e a estupidez 
humana. Mas, em relação ao universo, ainda não tenho certeza 
absoluta."

Albert Einstein

`

	letra := 'a'
	escape := "O importante é não parar de 'questionar'."

	fmt.Println(frase)
	fmt.Printf("%v %T\n", letra, letra)
	fmt.Println(escape)
}

Isso produz a seguinte saída:

"Duas coisas são infinitas: o universo e a estupidez  
humana. Mas, em relação ao universo, ainda não tenho certeza  
absoluta." 

Albert Einstein 


97 int32 
O importante é não parar de 'questionar'. 

Observe que a variável letra contém o valor Unicode da letra “a”, que é um int32 igual a 97.

Bytes e Runes

Strings podem ser convertidas em slices de bytes ou runes, e vice-versa:

package main

import (
	"fmt"
)

func main() {
	var a string = "imaginação"
	b := a[1:6]
	fmt.Println(a, b)
	ba := []byte(a)
	ra := []rune(a)

	fmt.Println(ba)
	fmt.Println(ra)
	fmt.Println("Bytes de [7:]:", string(ba[7:]), ba[7:])
	fmt.Println("Runes de [7:]:", string(ra[7:]), ra[7:])
}

A saída deste programa será:

imaginação magin
[105 109 97 103 105 110 97 195 167 195 163 111]
[105 109 97 103 105 110 97 231 227 111]
Bytes de [7:]: ção [195 167 195 163 111]
Runes de [7:]: ção [231 227 111]

Para caracteres convencionais, o retorno em bytes será idêntico ao runes, mas o mesmo não ocorre com caracteres acentuados e especiais.

No início deste artigo, mencionei que byte é um alias (apelido) para uint8, e isso requer atenção especial para não gerar resultados indesejados em seu código.

O código a seguir não imprimirá a string “109”, mas sim o caractere “m”:

package main

import (
	"fmt"
)

func main() {
	var a int = 109
	fmt.Println(string(a))
}

A conversão entre inteiro e string, e vice-versa, é realizada pelo pacote strconv, que abordarei em algum momento.

Maps

Em algumas situações, você pode desejar associar um valor a outro, e para isso, o Go oferece a função map. Sua sintaxe é apresentada abaixo:

map[keyType]valueType

O map gera uma estrutura semelhante a um dicionário em Python, mas com uma diferença importante: em um mapa em Go, todas as chaves devem ser do mesmo tipo, assim como todos os valores também devem ser do mesmo tipo.

No código a seguir, é criado um mapa de strings para números inteiros, associando os nomes de países às suas respectivas populações:

package main

import "fmt"

func main() {
	populacao := map[string]int64{
		"Brasil":         213317639,
		"China":          1411780000,
		"Índia":          1380004385,
		"Estados Unidos": 331449281,
		"Indonésia":      273523615,
		"Paquistão":      220892340,
	}

	fmt.Println(populacao)
}

Este código deve retornar um mapa como abaixo:

map[Brasil:213317639 China:1411780000 Estados Unidos:331449281 Indonésia:273523615 Paquistão:220892340 Índia:1380004385]

Observe que as linhas 7 a 12 têm uma vírgula no final, incluindo a última linha, para fazer a entrada de valores desta forma. Outra forma de inserir esses dados seria substituir as linhas 6 a 13 pelo código a seguir:

...
	populacao := map[string]int{}

	populacao["Brasil"] = 213317639
	populacao["China"] = 1411780000
	populacao["Índia"] = 1380004385
	populacao["Estados Unidos"] = 331449281
	populacao["Indonésia"] = 273523615
	populacao["Paquistão"] = 220892340
...

Outra maneira seria inserir todos os elementos em uma única linha:

...
    precos := map[string]float64{"banana": 4.99, "pera": 8.99, "cebola": 3.50}
...

Nesse caso, não é necessário adicionar uma vírgula antes do fechamento das chaves.

Quanto ao exemplo do mapa populacao, a implementação a seguir não funcionará conforme o desejado, embora o código não apresente erro de sintaxe:

...
    var populacao map[string]int

	populacao["Brasil"] = 213317639
	populacao["China"] = 1411780000
	populacao["Índia"] = 1380004385
	populacao["Estados Unidos"] = 331449281
	populacao["Indonésia"] = 273523615
	populacao["Paquistão"] = 220892340
...

Nesse caso, será gerado um mapa populacao nulo, nil, e um mapa nil possui um comprimento de 0. Uma tentativa de leitura desse map sempre retornará zero, mas uma tentativa de escrita gerará um pânico:

Bash
 
alves@arabel:map_panic$ go run map.go"
panic: assignment to entry in nil map
...

Esse comportamento ocorre devido à forma como os mapas funcionam em Go. Em Go, os mapas não são inicializados automaticamente quando declarados usando a sintaxe var. Eles começam com um valor zero, que é nil para mapas. Um mapa nil não pode ser usado para armazenar ou recuperar valores, pois não possui uma estrutura de dados interna apropriada para isso. Portanto, uma tentativa de escrita (atribuição) a um mapa nil gera um pânico.

Para corrigir este erro basta inicializar o mapa populacao, como segue:

...
    var populacao map[string]int

	populacao = map[string]int{} // iniciar com um mapa vazio

	populacao["Brasil"] = 213317639
	populacao["China"] = 1411780000
	populacao["Índia"] = 1380004385
	populacao["Estados Unidos"] = 331449281
	populacao["Indonésia"] = 273523615
	populacao["Paquistão"] = 220892340
...

Acessando os Valores de um map

O acesso ao conteúdo de um registro em um mapa é semelhante ao acesso a um array ou slice, basta passar a chave desejada entre colchetes. No entanto, existem algumas peculiaridades nos mapas:

  • Um acesso a uma chave no mapa retorna o valor e uma flag (um booleano) que indica se a busca foi bem-sucedida. Esta flag não é obrigatória de ser lida e pode ser simplesmente ignorada.
  • A busca por uma chave inexistente não gera erro, apenas retorna a flag como false.

O código a seguir ilustra isso:

package main

import (
	"fmt"
)

func main() {
	populacao := map[string]int{
		"Brasil":         213317639,
		"China":          1411780000,
		"Índia":          1380004385,
		"Estados Unidos": 331449281,
		"Indonésia":      273523615,
		"Paquistão":      220892340,
	}

	pop, ok := populacao["Brasil"]
	fmt.Println(pop, ok)

	pop, ok = populacao["Espanha"]
	fmt.Println(pop, ok)

	pop = populacao["China"]
	fmt.Println(pop)
}

O retorno deste código será:

Bash
 
alves@arabel:map2$ go run map2.go"
213317639 true
0 false
1411780000

As linhas 17 e 20 fazem a consulta no mapa pelos países “Brasil” e “Espanha“. A variável ok retorna true se a busca for bem-sucedida e false no caso de falha. A variável ok não precisa ser lida e pode ser omitida sem problemas, como ocorre na linha 23.

Excluindo Elementos de um Map

Para excluir elementos de um mapa, utilize o comando delete(map, key).

package main

import (
	"fmt"
)

func main() {
	populacao := map[string]int{
		"Brasil":         213317639,
		"China":          1411780000,
		"Índia":          1380004385,
		"Estados Unidos": 331449281,
		"Indonésia":      273523615,
		"Paquistão":      220892340,
	}

	delete(populacao, "Brasil")

	pop, ok := populacao["Brasil"]
	fmt.Println(pop, ok)
}

Este código retornará “0 false“, uma vez que o registro “Brasil” foi removido.

Operadores

Obviamente, os tipos em Go suportam diferentes operadores de acordo com o seu tipo. Para obter mais detalhes, leia na documentação do Go: The Go Programming Language Specification.

Operadores Aritméticos

A tabela a seguir apresenta os operadores aritméticos do Go:

OperadorNomeTipo
+somainteiros, ponto flutuante, complexos e strings
-subtraçãointeiros, ponto flutuante e complexos
*multiplicaçãointeiros, ponto flutuante e complexos
/divisãointeiros, ponto flutuante e complexos
%restointeiros
&bitwise ANDinteiros
|bitwise ORinteiros
^bitwise XORinteiros
&^bit clear (AND NOT)inteiros
<<deslocamento à esquerdainteiro << inteiro >=0
>>deslocamento à direitainteiro >> inteiro >=0

Os operadores aritméticos (+, -, * e /) se aplicam a valores numéricos e produzem resultados do mesmo tipo. O operador + também se aplica a strigs, concatenando seus valores. Operadores bitewise (bit a bit) e de deslocamento se aplicam apenas a inteiros.

Operadores de Comparação

Operadores de comparação comparam dois valores de um dado tipo e retornam um booliano verdadeiro/falso:

OperadorNomeTipo
==igualcomparáveis
!=diferentecomparáveis
<menor queinteiros, float e strings
>maior queinteiros, float e strings
<=menor ou igualinteiros, float e strings
>=maior ou igualinteiros, float e strings

São comparáveis booleanos, inteiros, floats, complexos e strings. Strings são comparadas lexicalmente byte por byte, enquanto ponteiros são iguais se apontarem para a mesma variável ou se forem nulos.

Arrays são iguais se seus elementos correspondentes forem iguais, e structs (vistos no próximo artigo) são iguais se os seus atributos não nulos forem iguais.

Outros tipos, como canais e interfaces, também podem ser comparados, mas são objetos de artigos que serão apresentados mais adiante.

Operadores Lógicos

Operadores lógicos atuam apenas sobre booleanos, alterando seu valor:

OperadorNomeDescrição
&&condicional ANDA && B retorna verdadeiro de A e B forem verdadeiros
||condicional ORA || B retorna verdadeiro se A ou B for verdadeiro
!operador de negação!A retorna verdadeiro de A for falso

Canais e Concorrência será apresentado adiante.

Precedência dos Operadores

Os operadores unários:

OperadorNomeDescrição
+xé o mesmo que 0 + x
-xnegaçãoé o mesmo que 0 - x
^xcomplemento lógico bit a bit (biwise)complemento bit a bit de x é calculado invertendo todos os bits no valor binário de x. É o mesmo que -x -1.

Operadores unários têm a precedência mais alta. Observe que na expressão “+x + y“, o “+” à frente do x é um operador unário, mas o “+” à frente do y é um operador de adição.

Os operadores ++ e -- são declarações e não expressões, por isso estão fora da hierarquia de operadores. Como consequência, a declaração *p++ é o mesmo que (*p)++.

Existem cinco níveis de precedência para operadores binários. Os operadores de multiplicação têm a maior precedência, seguidos pelos operadores de adição, operadores de comparação (&& lógico) e, finalmente, || (OU lógico). A tabela a seguir apresenta a precedência dos operadores:

PrioridadeOperadorObservação
1* / % << >> & &Multiplicativo
2+ - | ^Aditivo
3== != < > <= >=Comparativo
4&&E
5||OU

Considerações Finais

Este texto acabou sendo mais longo do que o esperado, mas essa extensão se justifica pela importância e riqueza do conteúdo abordado. Durante a exploração dos tipos básicos em Go, você teve a oportunidade de mergulhar em conceitos fundamentais para o domínio da linguagem.

Lembrando que a compreensão sólida desses fundamentos é crucial para qualquer desenvolvedor que deseja aproveitar ao máximo a eficiência e a flexibilidade oferecidas pelo Go. Compreender tipos numéricos, estruturas de dados como arrays, slices e maps, além de operadores e suas precedências, é essencial para construir aplicativos robustos e de alto desempenho.

É importante notar que, em linguagens como o Go, a construção do conhecimento é cumulativa. Portanto, mesmo que este texto tenha coberto uma ampla gama de tópicos, ainda há muito a explorar e a aprender. Quaisquer tópicos que porventura tenham sido esquecidos ou não puderam ser aprofundados neste texto serão abordados em revisões futuras.

No próximo artigo, continuaremos nossa jornada no mundo do Go, explorando o tipo struct. Essa estrutura é uma forma poderosa de implementar componentes no Go, oferecendo semelhança com classes, mas mantendo a simplicidade e a eficiência que a linguagem é conhecida por proporcionar.

Continue sua jornada de aprendizado, pratique os conceitos apresentados e, acima de tudo, divirta-se explorando as possibilidades que o Go oferece para o desenvolvimento de software de alta qualidade e alto desempenho. Obrigado por acompanhar este guia sobre tipos básicos em Go, e espero que você esteja ansioso para as emocionantes aventuras que estão por vir.

Deixe um comentário

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.