- Golang – 01. Introdução
- Golang – 02. Tipos Básicos
- Golang – 03. Structs e Funções e Métodos
- Golang – 04. Estruturas de Controle
- Golang – 05. Gerenciando Pacotes
- Golang – 06. Biblioteca Padrão I – fmt e strings
- Golang – 07. Biblioteca Padrão II – os, os/exec e os/user
- Golang – 08. Interface
- Golang – 09. Goroutines – Concorrência e Paralelismo
- Golang – 10. Goroutines – Canais
- Golang – 11. Pacotes e Documentação no Go
Índice
- 1. Tipos Numéricos
- 1.1. Declaração de Constantes
- 1.1.1. Constantes com iota
- 1.2. Declaração de Variáveis
- 1.3. Declaração via “var” ou “:=”
- 2. Arrays
- 3. Slices
- 3.1. Função Append
- 3.2. Função Capacidade e Comprimento
- 3.3. Função Make
- 3.4. Slicing Slices
- 3.4.1. Superposição inesperadas em Slices com Append
- 3.5. Função Copy
- 4. Strings
- 5. Bytes e Runes
- 6. Maps
- 6.1. Acessando os Valores de um map
- 6.2. Excluindo Elementos de um Map
- 7. Operadores
- 7.1. Operadores Aritméticos
- 7.2. Operadores de Comparação
- 7.3. Operadores Lógicos
- 7.4. Precedência dos Operadores
- 8. Considerações Finais
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ção | Descrição | Intervalo |
---|---|---|
uint8 | Inteiros positivos de 8-bits | 0 a 255 |
uint16 | Inteiros positivos de 16-bits | 0 a 65535 |
uint32 | Inteiros positivos de 32-bits | 0 a 4294967295 |
uint64 | Inteiros positivos de 64-bits | 0 a 18446744073709551615 |
int8 | Inteiros de 8-bits | -128 a 127 |
int16 | Inteiros de 16-bits | -32768 a 32767 |
int32 | Inteiros de 32-bits | -2147483648 a 2147483647 |
int64 | Inteiros de 64-bits | -9223372036854775808 a 9223372036854775807 |
float32 | Ponto flutuante 32-bits | até 3,4028234663852886e+38 |
float64 | Ponto flutuante 64-bits | até 1,7976931348623157e+308 |
complex64 | Complexo com float 32-bits para representar as partes real e imaginária | até (1+i)*3,4028234663852886e+38 |
complex128 | Complexo com float 64-bits para representar as parte real e imaginária | até (1+i)*1,7976931348623157e+308 |
byte | Alias para uint8 | 0 a 255 |
rune | Alias para uint32 | 0 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:
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:
alves@segrau:~$ go run "segrau.go"
Resolve uma equação de 2o grau
ax² + bx + c = 0Entre a, b, c:
2,5,-3
2.00x²+5.00x + -3.00 = 0.00
x1: 0.50 x2: -3.00Teste x1: 0
Teste x2: 0
alves@segrau:~$ go run segrau.goResolve 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:
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 dex
até o elemento de índicei2-1
. - Se omitirmos o índice
i2
(x[i1:]
), a fatia incluirá todos os elementos dex
a partir do elemento de índicei1
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:
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á:
alves@arabel:map2$ go run map2.go"
213317639 true0 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:
Operador | Nome | Tipo |
---|---|---|
+ | soma | inteiros, ponto flutuante, complexos e strings |
- | subtração | inteiros, ponto flutuante e complexos |
* | multiplicação | inteiros, ponto flutuante e complexos |
/ | divisão | inteiros, ponto flutuante e complexos |
% | resto | inteiros |
& | bitwise AND | inteiros |
| | bitwise OR | inteiros |
^ | bitwise XOR | inteiros |
&^ | bit clear (AND NOT) | inteiros |
<< | deslocamento à esquerda | inteiro << inteiro >=0 |
>> | deslocamento à direita | inteiro >> 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:
Operador | Nome | Tipo |
---|---|---|
== | igual | comparáveis |
!= | diferente | comparáveis |
< | menor que | inteiros, float e strings |
> | maior que | inteiros, float e strings |
<= | menor ou igual | inteiros, float e strings |
>= | maior ou igual | inteiros, 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:
Operador | Nome | Descrição |
---|---|---|
&& | condicional AND | A && B retorna verdadeiro de A e B forem verdadeiros |
|| | condicional OR | A || 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:
Operador | Nome | Descrição |
---|---|---|
+x | é o mesmo que 0 + x | |
-x | negação | é o mesmo que 0 - x |
^x | complemento 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:
Prioridade | Operador | Observaçã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.