Arrays
- Em Go, um array é uma sequência numerada de itens do mesmo tipo de tamanho específico.
- Os valores que um array contém são conhecidos como: itens ou elementos. Todos os elementos em um array devem ser do mesmo tipo. Depois que um array é criado, seu tamanho não pode ser alterado - ele não pode crescer ou diminuir.
A coleção de dados mais básica no Go é um array. Ao definir uma array, você deve especificar qual o tipo de dado que ele pode conter e o seu tamanho no seguinte formato: [<size>]<type>
. Por exemplo, [10]int
é um array que pode conter até 10 números inteiros, enquanto [5]string
é uma array de tamanho 5 que contém strings.
O ponto-chave para fazer um array, ser um array, é especificar o seu tamanho. Se na sua definição não tiver o seu tamanho, parece que funciona como um array, mas não seria - seria uma slice. Uma slice é um tipo de coleção diferente e mais flexível que falaremos após os arrays. Você pode definir os valores dos elementos de qualquer tipo, incluindo ponteiros e outros arrays.
Você pode inicializar arrays usando o seguinte formato: [<size>]<type>{<value1>,<value2>,…<valueN>}
. Por exemplo, [5]int{1}
inicializa o array com o primeiro valor de 1, enquanto que [5]int{9,9,9,9,9}
preenche o array com o valor 9 para cada elemento. Ao inicializar o array, você pode fazer com que Go defina seu tamanho com base no número de elementos com os quais você o inicializa. Você pode tirar proveito disso, substituindo o número do seu tamanho por ...
. Por exemplo, [...]int{9,9,9,9,9}
cria um array de tamanho 5 porque o inicializamos com 5 elementos. Assim como todos os arrays, o tamanho é definido em tempo de compilação e não pode ser alterado em runtime (tempo de execução).
Para declarar um array, você usa a seguinte sintaxe:
var array_name [size_of_array]data_type
Vamos definir um simples alguns array de tamanho 10 que contém números inteiros.
package main
import "fmt"
func defineArray() ([10]int, [10]int, [10]int) {
var arr1 [10]int
var arr2 [10]int = [10]int{1, 2, 3, 4}
arr3 := [10]int{4, 5, 6}
return arr1, arr2, arr3
}
func main() {
arr1, arr2, arr3 := defineArray()
fmt.Printf("%#v\n", arr1)
fmt.Printf("%#v\n", arr2)
fmt.Printf("%#v\n", arr3)
}
A execução do código anterior fornece a seguinte saída:
[10]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
[10]int{1, 2, 3, 4, 0, 0, 0, 0, 0, 0}
[10]int{4, 5, 6, 0, 0, 0, 0, 0, 0, 0}
No código acima, definimos três array, o primeiro sem ser inicializado, o segundo inicializando preenchendo apenas os 4 primeiros elementos/itens, e o terceiro preenchendo apenas os três primeiros itens. Como todos os arrays têm um tamanho fixo, quando o primeiro array foi impresso, ele continha 10 valores inicializadas com zero values para um int
, que é 0
. O segundo array continha apenas os 6 últimos elementos com seus zero values, e o terceiro os últimos 7 elementos preenchidos com zero values para um inteiro.
Todos os elementos no array devem ser do mesmo tipo especificado. Existem alguns estilos de declarações diferentes. No primeiro, você especifica o tamanho da array e o tipo dos elementos no array:
var x [3]int
Isso cria uma array de 3 inteiros. Como nenhum valor foi especificado, todas as posições (x[0]
, x[1]
e x[2]
) são inicializadas com zero value para um int
, que é 0
. Se você tiver valores iniciais para o array, você os especifica com um literal de array:
var x = [3]int{10, 20, 30}
Ao usar um literal de array para sua inicialização, você pode omitir o número do tamanho do array e usar ...
:
var x = [...]int{10, 20, 30}
O tamanho do array faz parte da sua definição de tipo. Se você tiver dois arrays que aceitam o mesmo tipo, mas têm tamanhos diferentes, eles não são compatíveis e não são comparáveis entre si. Arrays de tamanhos diferentes que não são do mesmo tipo não podem ser comparados uns com os outras. Você pode usar ==
e !=
Para comparar arrays:
package main
import "fmt"
func compArrays() (bool, bool, bool, bool, bool) {
var arr1 [5]int
arr2 := [5]int{0}
arr3 := [...]int{0, 0, 0, 0, 0}
arr4 := [5]int{0, 0, 0, 0, 9}
arr5 := [...]int{1, 2, 3}
arr6 := [3]int{1, 2, 3}
arr7 := [3]int{1, 1, 3}
return arr1 == arr2, arr1 == arr3, arr1 == arr4, arr5 == arr6, arr5 == arr7
}
func main() {
comp1, comp2, comp3, comp4, comp5 := compArrays()
fmt.Println("[5]int == [5]int{0} :", comp1)
fmt.Println("[5]int == [...]int{0, 0, 0, 0, 0}:", comp2)
fmt.Println("[5]int == [5]int{0, 0, 0, 0, 9} :", comp3)
fmt.Println("[...]int{1, 2, 3} == [3]int{1, 2, 3} :", comp4)
fmt.Println("[...]int{1, 2, 3} == [3]int{1, 1, 3} :", comp5)
}
Executar o código anterior produz a seguinte saída:
[5]int == [5]int{0} : true
[5]int == [...]int{0, 0, 0, 0, 0}: true
[5]int == [5]int{0, 0, 0, 0, 9} : false
[...]int{1, 2, 3} == [3]int{1, 2, 3} : true
[...]int{1, 2, 3} == [3]int{1, 1, 3} : false
Como a maioria das linguagens, os arrays em Go são lidos e escritos usando a sintaxe <array>[<index>]
. Por exemplo, isso acessa o primeiro elemento de um array, arr[0]
. Depois que o array é definido, você pode fazer alterações em elementos individuais usando seu índice, com a sintaxe <array>[<index>] = <value>
.
No código abaixo, definiremos um array e o inicializaremos com algumas palavras. Então, vamos ler as palavras na forma de uma mensagem e imprimi-la.
package main
import "fmt"
func message() string {
arr := [...]string{"ready", "Get", "Go", "to"}
arr[1] = "It's"
arr[0] = "time"
return fmt.Sprintln(arr[1], arr[0], arr[3], arr[2])
}
func main() {
fmt.Print(message())
}
Executar o código anterior produz a seguinte saída:
It's time to Go
Os arrays em Go raramente são usados explicitamente. Isso ocorre porque eles vêm com uma limitação incomum: Go considera o tamanho do array como parte do tipo do array. Isso faz com que um array declarado como [3]int
um tipo diferente de uma array declarada como [4]int
. Isso também significa que não pode ser usado uma variável para especificar o tamanho de um array, porque os tipos devem ser resolvidos em tempo de compilação, não em tempo de execução.
Além do mais, não pode ser usado conversão de tipo para converter arrays de tamanhos diferentes em tipos idênticos. Porque você não pode converter arrays de tamanhos diferentes entre si, você não pode escrever uma função que funcione com arrays de qualquer tamanho e você não pode atribuir arrays de tamanhos diferentes para a mesma variável.
Devido a essas restrições, não use arrays a menos que você saiba o tamanho que você precisa em tempo de execução.
Isso levanta uma questão: por que uma feature tão limitada na linguagem? A principal razão pela qual os arrays estão presentes no Go é fornecer o armazenamento de apoio para os slices, que são uma das features mais úteis do Go.
Slices
Os arrays são importante, mas sua inflexibilidade em relação ao seu tamanho pode causar problemas. Se você quisesse criar uma função que tem um parâmetro de array para classificação dos dados, ela só funcionaria com tamanho específico do array. Isso requer que você crie uma função para cada tamanho do array. Essa restrição faz com que a manipulação de arrays pareça complicado. O outro lado dos arrays é que eles são uma maneira eficiente de gerenciar coleções de dados classificados. Não seria ótimo se houvesse uma maneira de obter a eficiência dos arrays, mas com mais flexibilidade? Go oferece isso na forma de slices
.
Os slices
são construidos a partir dos arrays e permite que você tenha uma coleção indexada numérica classificada sem que você precise se preocupar com o tamanho. Go gerencia todos os detalhes, como o tamanho do array a ser usado. Você usa um slice
exatamente como faria com um array; ele contém apenas valores do mesmo tipo, você pode ler e gravar em cada elemento, e eles são fáceis de executar em loops.
A outra coisa que uma slice
pode fazer é ser facilmente ampliada usando a função append
. Esta função aceita sua slice
e os valores que você gostaria de adicionar e retorna uma nova slice
com tudo mesclado. É comum começar com uma slice
vazia e aumenta-la conforme necessário.
No código do mundo real, você deve usar slices
como seu destino para todas as coleções classificadas. Você será mais produtivo porque não precisará escrever tanto código quanto faria
com um array. A maioria dos códigos que você verá em projetos do mundo real usa muitas slices e raramente usa arrays.
Na maioria das vezes, quando você deseja uma estrutura de dados que contenha uma sequência de valores, um slice
é o que você usará. O que torna os slices tão úteis é que o tamanho não faz parte do seu tipo. Isso remove as limitações dos arrays. Podemos escrever uma única função que processa slices
de qualquer tamanho e podemos aumentá-los conforme necessário.
No exemplo de código a seguir, mostraremos como os slices
são flexíveis lendo alguns dados, passando uma slice
para uma função, fazendo loop, lendo valores e acrescentando valores ao final de uma slice
.
package main
import (
"fmt"
"os"
)
func getPassedArgs(minArgs int) []string {
if len(os.Args) < minArgs {
fmt.Printf("At least %v arguments are needed\n", minArgs)
os.Exit(1)
}
var args []string
for i := 1; i < len(os.Args); i++ {
args = append(args, os.Args[i])
}
return args
}
func findLongest(args []string) string {
var longest string
for i := 0; i < len(args); i++ {
if len(args[i]) > len(longest) {
longest = args[i]
}
}
return longest
}
func main() {
if longest := findLongest(getPassedArgs(3)); len(longest) > 0 {
fmt.Println("The longest word passed was:", longest)
} else {
fmt.Println("There was an error")
os.Exit(1)
}
}
Salve o arquivo. Em seguida, na pasta em que está salvo, execute o código usando o seguinte comando:
go run . Get ready to Go
Executar o código anterior produz a seguinte saída:
The longest word passed was: ready
No código acima, pudemos ver como os slices
são flexíveis e, ao mesmo tempo, como funcionam como arrays. Essa maneira de trabalhar com slices
é outra razão pela qual Go tem a sensação de uma linguagem dinâmica.
Manipular os slices
é muito parecido como manipular os arrays, mas existem diferenças sutis. A primeira coisa a notar é que não especificamos o tamanho do slice
quando é declarado:
var x = []int{10, 20, 30}
Isso cria um slice
de 3 ints usando um literal de slice
. Assim como os arrays, também podemos especificar apenas os índices com valores no literal de slice
:
var x = []int{1, 5: 4, 6, 10: 100, 15}
Isso cria um slice
de 12 ints com os seguintes valores: [1 0 0 0 0 4 6 0 0 0 100 15]
.
Você lê e grava slice
usando a sintaxe de colchetes:
x[0] = 10
fmt.Println(x[2])
Até agora, os slice
pareciam idênticos aos arrays. Começamos a ver as diferenças entre arrays e slice
quando examinamos a declaração do slice
sem usar um literal.
var x []int
Isso cria um slice
de ints. Uma vez que nenhum valor é atribuído, x
é atribuído para zero value
como nil
. Em Go, nil
é um identificador que representa a falta de um valor para alguns tipos. Um slice
com seu valor de nil
não contém nada.
Uma slice
é um tipo que não é comparável. É um erro em tempo de compilação usar ==
para ver se dois slices
são idênticos ou !=
para ver se são diferentes. A única coisa com a qual você pode comparar um slice
é com nil
.
fmt.Println(x == nil) // prints true
len
Go fornece várias funções integradas para trabalhar com seus tipos. A função len
é utilizada para saber o tamanho do slice
. Quando você passa um slice
como nil
para len
, ele retorna 0
.
append
A função append
é usada para adicionar mais de um valor a um slice
.
var x []int
x = append(x, 10)
A função append
leva pelo menos dois parâmetros, um slice
de qualquer tipo e um valor desse tipo. Ele retorna um slice
do mesmo tipo. O slice
retornado é atribuído de volta ao slice
que foi passado. No exemplo acima, estamos adicionando a um slice
nil
, mas você pode adicionar a uma slice
que já possui elementos:
var x = []int{1, 2, 3}
x = append(x, 4)
Você pode adicionar mais de um valor:
x = append(x, 5, 6, 7)
Um slice
pode ser adicionado a outro usando o operador variadic ...
para expandir o slice
de origem em valores individuais:
y := []int{20, 30, 40}
x = append(x, y...)
No código abaixo, usaremos o parâmetro variadic em append
para adicionar vários valores na forma de dados predefinidos a uma slice
. Em seguida, adicionaremos uma quantidade dinâmica de dados com base na entrada do usuário para a mesma slice
:
package main
import (
"fmt"
"os"
)
func getPassedArgs() []string {
var args []string
for i := 1; i < len(os.Args); i++ {
args = append(args, os.Args[i])
}
return args
}
func getLocals(extraLocals []string) []string {
var locales []string
locales = append(locales, "en_US", "fr_FR")
locales = append(locales, extraLocals...)
return locales
}
func main() {
locales := getLocals(getPassedArgs())
fmt.Println("Locales to use:", locales)
}
Salve o arquivo. Em seguida, na pasta criada aonde está o arquivo, execute o código usando o seguinte comando:
go run . fr_CN en_AU
Executar o código anterior produz a seguinte saída:
Locales to use: [en_US fr_FR fr_CN en_AU]
No código acima, usamos dois métodos para adicionar vários valores a uma slice
. Você também usaria essa técnica se precisasse mergear duas slices
.
cap
Como explicado acima, um slice
é uma sequência de valores. Cada elemento em uma slice
é atribuído a locais de memória consecutivos, o que torna mais rápido ler ou gravar esses valores. Cada slice
tem uma capacidade, que é o número de locais de memória consecutivos reservados. Isso pode ser maior do que o tamanho. Cada vez que a função append
é chamada em um slice
, um ou mais valores são adicionados ao seu final. Cada valor adicionado aumenta o tamanho em um. Quando o tamanho atinge a capacidade, não há mais espaço para colocar valores. Se você tentar adicionar valores quando o tamanho for igual à capacidade, a função append
usará o runtime do Go para alocar uma nova slice
com uma capacidade maior. Os valores da slice
original são copiados para a nova slice
, os novos valores são adicionados ao final e a nova slice
é retornada.
Quando uma slice
cresce por meio da função append
, leva tempo para o runtime do Go alocar nova memória e copiar os dados existentes da memória antiga para a nova. A memória antiga também precisa ser garbage collected. Por esse motivo, o runtime do Go geralmente aumenta uma slice
em mais de uma cada vez que fica sem capacidade. As regras do Go 1.16
são dobrar o tamanho da slice
quando a capacidade for inferior a 1024 e, em seguida, crescer por pelo menos 25%.
Assim como a função len
retorna o tamanho atual de um slice
, a função cap
retorna a capacidade atual de uma slice
. É usado com muito menos frequência do que len
. Na maioria das vezes, cap
é usado para verificar se uma slice
é grande o suficiente para conter novos dados ou se uma chamada a make
é necessária para criar uma nova slice
.
Você também pode passar um array para a função cap
, mas cap
sempre retorna o mesmo valor que len
para os arrays.
Vamos dar uma olhada em como adicionar elementos a uma slice
alterando o tamanho e a capacidade.
package main
import "fmt"
func main() {
var x []int
fmt.Println(x, len(x), cap(x))
x = append(x, 10)
fmt.Println(x, len(x), cap(x))
x = append(x, 20)
fmt.Println(x, len(x), cap(x))
x = append(x, 30)
fmt.Println(x, len(x), cap(x))
x = append(x, 40)
fmt.Println(x, len(x), cap(x))
x = append(x, 50)
fmt.Println(x, len(x), cap(x))
}
Ao executar o código, você verá a seguinte saída. Observe como e quando a capacidade aumenta:
[] 0 0
[10] 1 1
[10 20] 2 2
[10 20 30] 3 4
[10 20 30 40] 4 4
[10 20 30 40 50] 5 8
Embora seja bom que as slices
cresçam automaticamente, é muito mais eficiente dimensioná-las apenas uma vez. Se você sabe quantos elementos planeja colocar em uma slice
, crie a slice
com a capacidade inicial correta. Fazemos isso com a função make
.
make
A função make
do Go permite que você defina o tamanho e a capacidade de uma slice
ao criá-la. A sintaxe é semelhante a make(<sliceType>, <length>, <capacity>)
. Ao criar uma slice
usando make
, o argumento <capacity>
é opcional, mas o <length>
é obrigatório.
Já vimos duas maneiras de declarar uma slice
, usando um literal de slice
ou nil
zero value. Embora útil, nenhuma dessas maneira permite criar uma slice
vazia que já tenha um tamanho ou capacidade especificada. Esse é o trabalho da função make
. Permite-nos especificar o tipo, tamanho e, opcionalmente, a capacidade. Vamos dar uma olhada:
x := make([]int, 5)
Isso cria uma slice
com tamanho de 5 e capacidade de 5. Como tem tamanho de 5, x[0]
a x[4]
são elementos válidos e todos são inicializados com 0
.
Usando a função make
, criaremos algumas slices
e exibiremos seu tamanho e capacidade:
package main
import "fmt"
func genSlices() ([]int, []int, []int) {
var s1 []int
// Define a slice using make and set only the length:
s2 := make([]int, 10)
// Define a slice that uses both the length and capacity of the slices:
s3 := make([]int, 10, 50)
return s1, s2, s3
}
func main() {
s1, s2, s3 := genSlices()
fmt.Printf("s1: len = %v cap = %v\n", len(s1), cap(s1))
fmt.Printf("s2: len = %v cap = %v\n", len(s2), cap(s2))
fmt.Printf("s3: len = %v cap = %v\n", len(s3), cap(s3))
}
Executar o código anterior produz a seguinte saída:
s1: len = 0 cap = 0
s2: len = 10 cap = 10
s3: len = 10 cap = 50
No código anterior, usamos make
, len
e cap
para controlar e exibir o tamanho e a
capacidade de uma slice
ao definir uma.
Um erro comum é tentar preencher esses elementos iniciais usando append
.
x := make([]int, 5) // [0 0 0 0 0]
x = append(x, 10)
O número 10 é colocado no final da slice
, após os zero values nas posições 0-4
porque append
sempre aumenta o tamanho de uma slice
. O valor de x
agora é [0 0 0 0 0 10]
, com tamanho de 6 e capacidade de 10 (a capacidade foi dobrada assim que o 6º elemento foi anexado).
Também podemos especificar uma capacidade inicial com make
:
x := make([]int, 5, 10)
Isso cria uma slice
de inteiros com tamanho de 5 e capacidade de 10.
Você também pode criar uma slice
com tamanho zero, mas uma capacidade maior que zero:
x := make([]int, 0, 10)
Nesse caso, temos uma slice
non-nil com tamanho de zero, mas capacidade de 10. Como o tamanho é 0, não podemos indexar diretamente, mas podemos acrescentar valores:
x := make([]int, 0, 10)
x = append(x, 5, 6, 7, 8)
O valor da variável x
agora é [5 6 7 8]
, com tamanho de 4 e capacidade de 10.
Mais de make
Se você tem uma boa ideia do tamanho que sua slice
precisa ter, mas não sabe quais serão esses valores quando você estiver escrevendo o programa, use make
. A questão então é se você deve especificar um comprimento diferente de zero na chamada para make
ou especificar um comprimento zero e uma capacidade diferente de zero. Existem três possibilidades:
-
Se você estiver usando uma
slice
como buffer, especifique um comprimento diferente de zero. -
Se você tiver certeza de que sabe o tamanho exato que deseja, pode especificar o comprimento da
slice
para definir os valores. Isso geralmente é feito ao transformar valores em umaslice
. A desvantagem dessa abordagem é que, se você tiver o tamanho errado, acabará com zero values no final daslice
ou erros de panic ao tentar acessar elementos que não existem. -
Em outras situações, use
make
com comprimento zero e uma capacidade especificada. Isso permite que você useappend
para adicionar itens àslice
. Se o número de itens acabar sendo menor, você não terá zero value no final. Se o número de itens for maior, seu código não entrará em panic.
Slicing Slices - Criação de Slices a partir de Slices e Arrays
Uma expressão de slice
cria uma slice
de uma slice
. É escrito entre colchetes e consiste em um offset inicial e um offset final, separados por dois pontos (:
). A notação mais comum é [:]
. Se você não especificar o valor do offset inicial, 0
será assumido. Da mesma forma, se você não especificar valor do offset final, até o final da slice
será utilizado. Você pode ver como isso funciona executando o seguinte código:
package main
import "fmt"
func main() {
x := []int{1, 2, 3, 4}
y := x[:2]
z := x[1:]
d := x[1:3]
e := x[:]
fmt.Println("x:", x)
fmt.Println("y:", y)
fmt.Println("z:", z)
fmt.Println("d:", d)
fmt.Println("e:", e)
}
Ele fornece a seguinte saída:
x: [1 2 3 4]
y: [1 2]
z: [2 3 4]
d: [2 3]
e: [1 2 3 4]
No código abaixo, usaremos a notação de intervalo de slice
para criar slices
com uma variedade de valores. Normalmente, no código das aplicações, você precisa trabalhar apenas com uma pequena parte de um slice
ou array. A notação de intervalo é uma maneira rápida e direta de obter apenas os dados de que você precisa.
package main
import "fmt"
func message() string {
s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
m := fmt.Sprintln("First :", s[0], s[0:1], s[:1])
m += fmt.Sprintln("Last :", s[len(s)-1], s[len(s)-1:len(s)], s[len(s)-1:])
m += fmt.Sprintln("First 5 :", s[:5])
m += fmt.Sprintln("Last 4 :", s[5:])
m += fmt.Sprintln("Middle 5:", s[2:7])
return m
}
func main() {
fmt.Print(message())
}
Executar o código anterior produz a seguinte saída:
First : 1 [1] [1]
Last : 9 [9] [9]
First 5 : [1 2 3 4 5]
Last 4 : [6 7 8 9]
Middle 5: [3 4 5 6 7]
No código anterior, tentamos algumas maneiras de criar slices
a partir de outra slice
. Você também pode usar essas mesmas técnicas em um array como outro array. Vimos que os índices inicial e final são opcionais. Se você não tiver um índice inicial, ele começará no início da slice
ou array de origem. Se você não tiver o índice final, ele irá até no final do array. Se você não especificar os índices inicial e final, será feita uma cópia da slice
ou array. Este truque é útil para transformar arrays em um slice, mas não é útil para copiar slices
porque as duas slices
compartilham o mesmo endereço de memória.
Slices share storage sometimes
Quando você cria uma slice
de uma outra slice
, não está fazendo uma cópia dos dados. Em vez disso, agora você tem duas variáveis que estão compartilhando o mesmo endereço de memória. Isso significa que as alterações em um elemento de uma slice
afetam todas as slices
que compartilham esse elemento. Vamos ver o que acontece quando alteramos os valores.
package main
import "fmt"
func main() {
x := []int{1, 2, 3, 4}
y := x[:2]
z := x[1:]
x[1] = 20
y[0] = 10
z[1] = 30
fmt.Println("x:", x)
fmt.Println("y:", y)
fmt.Println("z:", z)
}
Você obtém a seguinte saída:
x: [10 20 30 4]
y: [10 20]
z: [20 30 4]
Como você pode ver, alterar x
modificou y
e z
, enquanto alterações em y
e z
modificaram x
.
Convertendo Arrays em Slices
Slices não são a única coisa que você pode fatiar. Se você tiver um array, poderá obter uma slice
usando uma slice expression. Esta é uma maneira útil de conectar um array a uma função que espera receber slices
. No entanto, esteja ciente de que tirar uma slice
de uma array tem as mesmas propriedades de compartilhamento de memória que tirar uma slice
de uma outra slice
.
package main
import (
"fmt"
)
func main() {
x := [4]int{5, 6, 7, 8}
y := x[:2]
z := x[2:]
x[0] = 10
fmt.Println("x:", x)
fmt.Println("y:", y)
fmt.Println("z:", z)
}
Você obtém o resultado:
x: [10 6 7 8]
y: [10 6]
z: [7 8]
copy
Se você precisar criar uma nova slice
independente do original, use a função de copy
. Vamos dar uma olhada em um simples exemplo.
package main
import "fmt"
func main() {
x := []int{1, 2, 3, 4}
y := make([]int, 4)
num := copy(y, x)
fmt.Println(y, num)
}
Você obtém o resultado:
[1 2 3 4] 4
A função copy
leva dois parâmetros. O primeiro é a slice
de destino e o segundo é a slice
de origem. Ele copia quantos valores puder da origem ao destino, limitado por qualquer slice
que seja menor, e retorna o número de elementos copiados. A capacidade de x
e y
não importa, é o tamanho/comprimento que importa.
Você não precisa copiar uma slice
inteira. O código a seguir copia os dois primeiros elementos de uma slice
de quatro elementos de uma slice
de dois elementos:
x := []int{1, 2, 3, 4}
y := make([]int, 2)
num = copy(y, x)
A variável y
é definida como [1 2]
e num
é definido como 2.
Você também pode copiar do meio da slice
de origem:
x := []int{1, 2, 3, 4}
y := make([]int, 2)
copy(y, x[2:])
Estamos copiando o terceiro e o quarto elementos em x
, obtendo uma slice
da slice
. Observe também que não atribuímos a saída da copy
a uma variável. Se você não precisa do número de elementos copiados, não precisa atribuí-lo.
A função de copy
permite que você copie entre duas slices
que cobrem as seções sobrepostas de uma slice
subjacente:
x := []int{1, 2, 3, 4}
num = copy(x[:3], x[1:])
fmt.Println(x, num)
Neste caso, estamos copiando os últimos três valores em x
sobre os três primeiros valores de x
. Isso imprime [2 3 4 4] 3
.
Você pode usar copy
com array pegando uma parte do array. Você pode tornar o array a origem ou o destino de copy
.
x := []int{1, 2, 3, 4}
d := [4]int{5, 6, 7, 8}
y := make([]int, 2)
copy(y, d[:])
fmt.Println(y)
copy(d[:], x)
A primeira copy
copia os dois últimos valores do array d
para a slice
y
. A segunda copia todos os valores da slice
x
para o array d
. Isso produz a saída:
[5 6]
[1 2 3 4]
Maps
As slices
são úteis quando você tem dados sequenciais. Como a maioria das linguagens, Go fornece um tipo de dado para situações em que você deseja associar um valor a outro. O tipo de map
é escrito usando a notação map[<keyType>]<valueType>
. Vamos dar uma olhada em algumas maneiras diferentes de declarar maps
. Primeiro, você pode usar uma declaração var
para criar uma variável de map
definida com zero value:
var nilMap map[string]int
Nesse caso, a variável nilMap
declara um map
com chaves de string
e valores int
. O zero value para um map
é nil
. Um map
nil
tem um tamanho de 0
. A tentativa de ler um mapa nil
sempre retorna zero value para o tipo de valor do mapa. No entanto, a tentativa de gravar em uma variável de mapa como nil
causa pânico.
Podemos usar a declaração :=
para criar uma variável de map
atribuindo a ela um map literal:
totalWins := map[string]int{}
Nesse caso, estamos usando um map literal vazio. Não é o mesmo que um map
como nil
. Ele tem tamanho zero, mas você pode ler e escrever um map
criado como um map literal vazio. Esta é a aparência de um map literal não vazio:
teams := map[string][]string {
"Orcas": []string{"Fred", "Ralph", "Bijou"},
"Lions": []string{"Sarah", "Peter", "Billie"},
"Kittens": []string{"Waldo", "Raul", "Ze"},
}
O corpo de um map literal é escrito com a chave, seguido por dois pontos (:
) e o seu valor. Há uma vírgula separando cada par de chave-valor no mapa, mesmo na última linha. Neste exemplo, o valor é uma slice
de strings. O tipo do valor em um mapa pode ser qualquer coisa. Existem algumas restrições aos tipos de chaves que discutiremos em breve.
Se você sabe quantos pares de chave-valor pretende colocar no map
, mas não sabe os valores exatos, pode usar make
para criar um map
com um tamanho padrão:
ages := make(map[int][]string, 10)
Os maps
criados com make
ainda têm tamanho de 0
e podem crescer além do tamanho especificado inicialmente.
Os maps
são como slices
de várias maneiras:
- Os
maps
aumentam automaticamente à medida que você adiciona pares de chave-valor a eles. - Se você sabe quantos pares de chave-valor planeja inserir em um
map
, você pode usarmake
para criar ummap
com um tamanho inicial específico. - Passar um
map
para a funçãolen
, informará o número de pares de chave-valor. - O zero value para um
map
énil
. - Os
maps
não são comparáveis. Você pode verificar se eles são iguais anil
, mas não pode verificar se doismaps
têm chaves e valores idênticos usando==
ou diferem usando!=
.
A chave de um map
pode ser qualquer tipo comparável. Isso significa que você não pode usar uma slice
ou outro map
como chave para um map
.
Quando você deve usar um map
e quando você deve usar uma slice
? As slices
são para listas de dados, especialmente para dados que são processados sequencialmente. Os maps
são úteis quando você tem dados organizados/relacionados de acordo com um valor e que a ordem não importa.
DICA: use um map
quando a ordem dos elementos não for importante. Use uma slice
quando a ordem dos elementos for importante.
Ler e escrever
Você nem sempre saberá se existe uma chave em um map
antes de precisar usá-la para obter um valor. Quando você está obtendo um valor para uma chave que não existe em um map
, Go retorna o zero value para o tipo de valor do map
. Ter uma lógica que manipule o zero values é uma forma válida de programar em Go, mas nem sempre é possível. Se você não puder usar a lógica de verificação do zero value, os maps
podem retornar um valor de retorno extra. A notação parece com <value>, <exists_value> := <map>[<key>]
.<exists_value>
é um valor booleano que é verdadeiro se houver uma chave no map
; caso contrário, é falso. Ao fazer um loop, você deve usar a palavra-chave range
. Ao percorrer um map
, nunca confie na ordem dos itens nele. Go não garante a ordem dos itens em um map
. Para garantir que ninguém tenha responsabilidade sobre a ordem dos elementos, Go propositadamente randomiza a ordem deles quando é percorrido. Se você precisar fazer um loop sobre os elementos de seu map
em uma ordem específica, precisará usar um array ou slice
para ajudá-lo com isso.
Vejamos um pequeno programa que declara, escreve e lê de um mapa:
package main
import "fmt"
func main() {
totalWins := map[string]int{}
totalWins["Orcas"] = 1
totalWins["Lions"] = 2
fmt.Println(totalWins["Orcas"])
fmt.Println(totalWins["Kittens"])
totalWins["Kittens"]++
fmt.Println(totalWins["Kittens"])
totalWins["Lions"] = 3
fmt.Println(totalWins["Lions"])
}
Ao executar o código acima, você verá a seguinte saída:
1
0
1
3
Atribuímos um valor a uma chave do map
colocando a chave entre colchetes e usando o sinal de atribuição =
para especificar o valor e lemos o valor do map
colocando a chave entre colchetes. Observe que você não pode usar :=
para atribuir um valor a uma chave do map
.
Quando tentamos acessar uma chave do map
que nunca foi definida, o map
retorna o zero value para o tipo de seu valor. Nesse caso, o tipo do valor é um int
, então obtemos um 0
. Você pode usar o operador ++
para incrementar um valor numérico. Como um map
retorna o zero value por padrão, isso funciona mesmo quando não há nenhum valor existente associado à chave.
Comma ok Idiom
Como vimos, um map
retorna o zero value se você solicitar o valor associado a uma chave que não está no map
. Isso é útil ao implementar coisas como o contador que vimos anteriormente. No entanto, às vezes você precisa descobrir se uma chave existe em um map
. Go fornece o comma ok idiom para dizer a diferença entre uma chave que está associada a um zero value e uma chave que não está no map
. Use a seguinte notação para fazer isso: <value>, <exists_value> := <map>[<key>]
.
m := map[string]int{
"hello": 5,
"world": 0,
}
v, ok := m["hello"]
fmt.Println(v, ok) // 5 true
v, ok = m["world"]
fmt.Println(v, ok) // 0 true
v, ok = m["goodbye"]
fmt.Println(v, ok) // 0 false
Em vez de atribuir o resultado de uma leitura do map
a uma única variável, com o comma ok idiom você atribui os resultados de uma leitura do map
a duas variáveis. O primeiro obtém o valor associado à chave. O segundo valor retornado é um bool
. Geralmente é denominado ok
. Se ok
for verdadeiro, a chave está presente no map
. Se ok
for falso, a chave não está presente. Neste exemplo, o código imprime 5 true, 0 true, and 0 false
.
Vamos ver outro exemplo, criando uma lógica para imprimir os dados do map
com base na chave que é passada para o programa.
package main
import (
"fmt"
"os"
)
// Define a map and initialize it with data. Then, return the map and close the function:
func getUsers() map[string]string {
return map[string]string{
"305": "User 1",
"204": "User 2",
"631": "User 3",
"073": "User 4",
}
}
func getUser(id string) (string, bool) {
users := getUsers()
user, exists := users[id]
return user, exists
}
func main() {
if len(os.Args) < 2 {
fmt.Println("User ID not passed")
os.Exit(1)
}
userID := os.Args[1]
name, exists := getUser(userID)
// If the key is not found, print a message, and then print all the users using a range loop. After that, exit:
if !exists {
fmt.Printf("Passed user ID (%v) not found.\nUsers:\n", userID)
for key, value := range getUsers() {
fmt.Println(" ID:", key, "Name:", value)
}
os.Exit(1)
}
fmt.Println("Name:", name)
}
Executar o código com o seguinte comando:
go run main.go 123
Produz a seguinte saída:
Passed user ID (123) not found.
Users:
ID: 073 Name: User 4
ID: 305 Name: User 1
ID: 204 Name: User 2
ID: 631 Name: User 3
exit status 1
Executar o código com o seguinte comando:
go run main.go 305
Produz a seguinte saída:
Name: User 1
Neste código acima, aprendemos como podemos verificar se existe uma chave em um map
. Pode parecer um pouco estranho vindo de outras linguagens que exigem que você verifique a existência de uma chave antes de obter o valor, não depois. Essa maneira de fazer as coisas significa que há muito menos chance de erros em runtime. Se um zero value não for possível em sua lógica de domínio, você pode usar esse forma para verificar se existe uma chave.
Usamos um loop com range
para imprimir todos os usuários no map
. Sua saída provavelmente está em uma ordem diferente da saída mostrada do resultado acima. Isso se deve ao fato de Go randomiza a ordem dos elementos em um map
quando você usa range
.
Removendo chaves
Para remover um elemento no map
, precisamos usar a função delete
. A assinatura da função de delete
, quando usada com maps
, é delete(<map>, <key>)
. A função não retorna nada e se uma chave não existe, nada acontece.
m := map[string]int{
"hello": 5,
"world": 10,
}
delete(m, "hello")
A função delete
recebe um map
e uma chave e, em seguida, remove o elemento com a chave especificada. Se a chave não estiver presente no map
ou se o map
for nil
, nada acontece. A função de delete
não retorna um valor.
No código abaixo, definiremos um map
e, em seguida, excluiremos um elemento dele usando a entrada do usuário. Em seguida, imprimiremos o map
no console sem o usuário que foi deletado:
package main
import (
"fmt"
"os"
)
var users = map[string]string{
"305": "User 1",
"204": "User 2",
"631": "User 3",
"073": "User 4",
}
func deleteUser(id string) {
delete(users, id)
}
func main() {
if len(os.Args) < 2 {
fmt.Println("User ID not passed")
os.Exit(1)
}
userID := os.Args[1]
deleteUser(userID)
fmt.Println("Users:", users)
}
Execute o código usando o seguinte comando:
go run . 305
Execução do código anterior produz a seguinte saída:
Users: map[073:User 4 204:User 2 631:User 3]
No código acima, usamos a função delete
para remover um elemento de um map
. Este requisito é exclusivo para mapas; você não pode usar a função delete
em arrays ou slices.
Simple Custom Types
Go permite que você declare um tipo em qualquer nível, do bloco de pacote para baixo. No entanto, você só pode acessar o tipo de dentro de seu escopo. As únicas exceções são os tipos de nível de bloco de pacote exportados.
Você pode criar tipos personalizados usando os tipos simples do Go como ponto de partida. A notação é type <name> <type>
. Se tivéssemos que criar um tipo de ID baseado em uma string, isso se pareceria com type id string
. O tipo personalizado age da mesma forma que o tipo no qual você o baseou, incluindo obter o mesmo zero value e ter as mesmas habilidades para comparar com outros valores do mesmo tipo. Um tipo personalizado não é compatível com seu tipo base, mas você pode converter seu tipo personalizado de volta no tipo base para permitir a interação.
No código abaixo, definiremos um map
e, em seguida, excluiremos um elemento usando o input do usuário. Em seguida, imprimiremos o map
menor no console.
package main
import "fmt"
type id string
func getIDs() (id, id, id) {
var id1 id
var id2 id = "1234-5678"
var id3 id
id3 = "1234-5678"
return id1, id2, id3
}
func main() {
id1, id2, id3 := getIDs()
fmt.Println("id1 == id2 :", id1 == id2)
fmt.Println("id2 == id3 :", id2 == id3)
fmt.Println("id2 == \"1234-5678\":", string(id2) == "1234-5678")
}
Executar o código anterior produz a seguinte saída:
id1 == id2 : false
id2 == id3 : true
id2 == "1234-5678": true
Neste exemplo, criamos um tipo personalizado, configuramos seus dados, em seguida, comparamos com valores do mesmo tipo e com seu tipo base.
Tipos personalizados são uma parte fundamental da modelagem dos problemas de dados que você verá nas aplicações. Ter tipos projetados para refletir os dados com os quais você precisa trabalhar ajuda a manter seu código fácil de entender e manter.
Structs
Os mapas são uma maneira conveniente de armazenar alguns tipos de dados, mas eles têm limitações. Eles não definem uma API, pois não há como restringir um mapa para permitir apenas certas chaves. Todos os valores em um mapa também devem ser do mesmo tipo. Por essas razões, os mapas não são a maneira ideal de passar dados de função para função. Quando você tiver dados relacionados que deseja agrupar, deve definir uma struct
.
As coleções são perfeitas para agrupar valores do mesmo tipo e finalidade. Há uma outra maneira de agrupar dados no Go para diferentes finalidades. Frequentemente, uma string simples, número ou booleano não captura totalmente a essência dos dados que você terá.
Por exemplo, para o exemplo acima do mapa de usuário, um usuário era representado por seu ID exclusivo e seu primeiro nome. Raramente haverá detalhes suficientes para trabalhar com os registros do usuário. Os dados que você pode capturar sobre uma pessoa são quase infinitos. Seria possível armazenar esses dados em vários mapas, todos com a mesma chave, mas isso é difícil de trabalhar e manter.
A coisa ideal a fazer é coletar todos esses diferentes bits de dados em uma única estrutura de dados que você pode projetar e controlar. Esse é o tipo struct
do Go: é um tipo personalizado que você pode nomear e especificar as propriedades com seus tipos.
NOTA: Se você já conhece uma linguagem orientada a objetos, pode estar se perguntando sobre a diferença entre classes e structs
. A diferença é simples: Go não tem classes, porque não tem herança. Isso não significa que Go não tenha alguns dos recursos das linguagens orientadas a objetos, ele deve fazer as coisas de uma maneira um pouco diferente.
A notação das structs
é a seguinte:
type <name> struct {
<fieldName1> <type>
<fieldName2> <type>
…
<fieldNameN> <type>
}
Os nomes dos campos (fieldName1
, fieldName2
...) devem ser exclusivos dentro de uma struct
. Você pode usar qualquer tipo para um campo, incluindo ponteiros, coleções e outras structs
.
Você pode acessar um campo em uma struct
usando a seguinte notação: <structValue>.<fieldName>
. Para definir o valor, você usa a notação: <structValue>.<fieldName> = <value>
. Para ler um valor, você usa a seguinte notação: value = <structValue>.<fieldName>
.
Structs é a coisa mais próxima que Go tem do que são chamadas de classes em outras linguagens, mas structs
foram propositalmente mantidos pequenos pelos designers do Go. A principal diferença é que as structs
não têm nenhuma forma de herança. Os designers do Go sentem que a herança causa mais problemas do que resolve no código.
Depois de definir seu tipo de struct
personalizada, você pode usá-lo para criar um valor. Você tem várias maneiras de criar valores a partir de tipos de struct
.
A maioria das linguagens tem um conceito semelhante a struct
e a sintaxe que Go usa para ler e escrever structs
deve ser familiar.
type person struct {
name string
age int
}
Depois que um tipo de estrutura é declarado, podemos definir variáveis desse tipo.
var jonh person
Estamos usando uma declaração var
. Como nenhum valor é atribuído a jonh
, ele obtém o zero value para o tipo da struct
de person
. Uma struct
zero value tem cada campo definido com o seu próprio zero value.
Um literal de struct
também pode ser atribuído a uma variável.
bob := person{}
Ao contrário dos mapas, não há diferença entre atribuir um literal de struct vazia e não atribuir um valor. Ambos inicializam todos os campos na struct
com os zero values dos campos. Existem dois estilos diferentes para um literal de struct
não vazio. Um literal de struct
pode ser especificado como uma lista de valores separados por vírgulas para os campos entre colchetes:
jonh := person {
"Jonh",
30,
}
Ao usar este formato literal de struct
, um valor para cada campo na struct
deve ser especificado e os valores são atribuídos aos campos na ordem em que foram declarados na definição da struct
.
O segundo estilo literal de struct
se parece com o estilo literal de map
:
nick := person {
age: 30,
name: "Nick",
}
Você usa os nomes dos campos na struct
para especificar os valores. Qualquer campo não especificado é definido com seu zero value. Você não pode misturar os dois estilos literais de struct
; ou todos os campos são especificados com chaves ou nenhum deles é. Para pequenas structs
onde todos os campos são sempre especificados, o estilo literal de estrutura mais simples é adequado. Em outros casos, use os nomes das chaves. É mais detalhado, mas deixa claro qual valor está sendo atribuído a qual campo sem precisar fazer referência à definição da struct
. Também é mais fácil de manter. Se você inicializar uma struct
sem usar os nomes de campo e uma versão futura precisar adicionar campos adicionais, seu código não será mais compilado.
Agora, se você deseja acessar as diferentes partes dos dados armazenados na struct
, você pode simplesmente usar o operador de ponto ".
":
bob.name = "Bob"
fmt.Println(bob.name)
Assim como usamos colchetes para ler e escrever em um map
, usamos a notação de ponto para ler e escrever nos campos da struct
.
No exemplo de código abaixo, vamos definir uma struct
de usuário. Vamos definir alguns campos de diferentes tipos. Em seguida, criaremos alguns valores da struct
usando alguns métodos diferentes.
package main
import "fmt"
type user struct {
name string
age int
balance float64
member bool
}
func getUsers() []user {
u1 := user{
name: "User 1",
age: 20,
balance: 78.43,
member: true,
}
u2 := user{
age: 19,
name: "User 2",
}
u3 := user{
"User 3",
25,
0,
false,
}
var u4 user
u4.name = "User 4"
u4.age = 31
u4.member = true
u4.balance = 17.09
return []user{u1, u2, u3, u4}
}
func main() {
for key, user := range getUsers() {
fmt.Printf("%v: %#v\n", key, user)
}
}
Executar o código anterior produz a seguinte saída:
0: main.user{name:"User 1", age:20, balance:78.43, member:true}
1: main.user{name:"User 2", age:19, balance:0, member:false}
2: main.user{name:"User 3", age:25, balance:0, member:false}
3: main.user{name:"User 4", age:31, balance:17.09, member:true}
No código anterior, você definiu um tipo de struct
personalizada que continha vários campos, cada um de um tipo diferente. Em seguida, criamos valores dessa estrutura usando alguns métodos diferentes. Cada um desses métodos é válido e útil em diferentes contextos.
Definimos a struct
no escopo do pacote e, embora não seja típico, você pode definir os tipos de struct
no escopo da função também. Se você definir uma struct
em uma função, ela só será válida para uso nessa função. Ao definir um tipo no nível do pacote, ele está disponível para uso em todo o pacote.
Transformando uma struct
por meio de seu ponteiro passado para uma função:
type User struct {
name string
age int
}
func main() {
newUser := User{"User 1", 19}
fmt.Println("Old Age", newUser.age)
incrementAge(&newUser)
fmt.Println("Age", newUser.age)
}
func incrementAge(user *User) {
user.age++
fmt.Println("New Age", user.age)
}
Old Age 19
New Age 20
Age 20
Estruturas anônimas
Você também pode declarar que uma variável implementa um tipo de struct
sem primeiro dar um nome ao tipo da struct
. Isso é chamado de estrutura anônima.
var person struct {
name string
age int
}
person.name = "bob"
person.age = 50
pet := struct {
name string
kind string
} {
name: "Dog name",
kind: "dog",
}
Neste exemplo, os tipos das variáveis person
e pet
são estruturas anônimas. Você atribui (e lê) campos em uma estrutura anônima da mesma forma que uma estrutura nomeada. Assim como você pode inicializar uma instância de uma estrutura nomeada com uma estrutura literal, você também pode fazer o mesmo para uma estrutura anônima.
Você pode se perguntar quando é útil ter tipo de dados que está associado apenas a uma única instância. Existem duas situações comuns em que estruturas anônimas são úteis. A primeira é quando você converte dados externos em uma estrutura ou uma estrutura em dados externos (como JSON ou buffers). Isso é chamado de marshaling (empacotamento) e unmarshaling (desempacotamento) de dados. Escrever testes é outro lugar onde estruturas anônimas aparecem.
Comparando e convertendo estruturas
Se uma
struct
é ou não comparável depende de seus campos. Asstructs
inteiramente compostas de tipos comparáveis são comparáveis; aqueles com campos deslices
oumaps
não são. Se astruct
foi definida anonimamente e tem a mesma estrutura de umastruct
nomeada, Go permite a comparação.
No exemplo de código abaixo, definiremos uma struct
comparável e criaremos um valor com ela. Também definiremos e criaremos valores com structs
anônimas que têm a mesma estrutura de nossa struct
nomeada. Finalmente, iremos compará-los e imprimir os resultados no console.
package main
import "fmt"
type point struct {
x int
y int
}
func compare() (bool, bool) {
point1 := struct {
x int
y int
}{
10,
10,
}
point2 := struct {
x int
y int
}{}
point2.x = 10
point2.y = 5
point3 := point{10, 10}
return point1 == point2, point1 == point3
}
func main() {
a, b := compare()
fmt.Println("point1 == point2:", a)
fmt.Println("point1 == point3:", b)
}
Executar o código anterior produz a seguinte saída:
point1 == point2: false
point1 == point3: true
No código anterior, vimos que podemos trabalhar com valores de struct
anônimas da mesma maneira que tipos de struct
nomeadas, incluindo suas comparações. Com tipos nomeados, você só pode comparar structs
do mesmo tipo. Quando você compara tipos em Go, Go compara todos os campos para verificar se há uma correspondência. Go está permitindo que uma comparação dessas structs
anônimas seja feita porque os nomes e tipos de campo correspondem. Go é um pouco flexível ao comparar structs
como esta.
Em Go não é permito comparações entre variáveis de tipos primitivos diferentes, Go não permite comparações entre variáveis que representam structs
de tipos diferentes. Go permite que você execute uma conversão de tipo, de um tipo de struct
para outro se os campos de ambas as structs
tiverem os mesmos nomes, ordem e tipos. Vamos ver o que isso significa. Dada esta struct
:
type firstPerson struct {
name string
age int
}
Podemos usar uma conversão de tipo para converter uma instância de firstPerson
em secondPerson
, mas não podemos usar ==
para comparar uma instância de firstPerson
e uma instância de secondPerson
, porque são tipos diferentes:
type secondPerson struct {
name string
age int
}
Não podemos converter uma instância de firstPerson
em thirdPerson
, porque os campos estão em uma ordem diferente:
type thirdPerson struct {
age int
name string
}
Não podemos converter uma instância de firstPerson
em fourthPerson
porque os nomes dos campos não correspondem:
type fourthPerson struct {
firstName string
age int
}
Por fim, não podemos converter uma instância de firstPerson
em fifthPerson
porque há um campo adicional:
type fifthPerson struct {
name string
age int
favoriteColor string
}
As structs
anônimas adicionam uma pequena diferença a isso: se duas variáveis de struct
estão sendo comparadas e pelo menos uma delas tem um tipo que é uma struct
anônima, você pode compará-las sem uma conversão de tipo se os campos de ambas as structs
tiverem os mesmos nomes, ordem, e tipos. Você também pode atribuir entre tipos de structs
nomeadas e anônimas se os campos de ambas as structs
tiverem os mesmos nomes, ordem e tipos.
type firstPerson struct {
name string
age int
}
f := firstPerson{
name: "Bob",
age: 50,
}
var g struct {
name string
age int
}
// compiles -- can use = and == between identical named and anonymous structs
g = f
fmt.Println(f == g) // true
Usando composição na Struct com incorporação (embedding)
Embora a herança não seja possível com structs
em Go, os designers de Go incluíram uma alternativa interessante. A alternativa é incorporar tipos em tipos de struct
. Usando a incorporação, você pode adicionar campos a uma struct
a partir de outras structs
. Este recurso de composição permite adicionar a uma struct
usando outras structs
como componentes. Embedding (incorporação) é diferente de ter um campo do tipo struct
. Quando você embedded (incorpora), os campos da struct
incorporada são promovidos. Depois de promovido, um campo age como se estivesse definido na struct
de destino.
Para embed (incorporar) uma struct
, você a adiciona como faria com um campo, mas não especifica um nome. Para fazer isso, você adiciona o nome do tipo da struct
a outra struct
sem dar a ela um nome do campo, que se parece com isto:
type <name> struct {
<Type>
}
Há duas maneiras válidas de trabalhar com campos incorporados em uma estrutura: <structValue>.<fieldName>
ou <structValue>.<type>.<fieldName>
. Essa capacidade de acessar o tipo por seu nome também significa que os nomes do tipo devem ser exclusivos entre os tipos incorporados e os nomes dos campos raiz. Ao incorporar tipos de ponteiro, o nome do tipo é o tipo sem a notação de ponteiro, portanto, o nome *<type>
torna-se <type>
. O campo ainda é um ponteiro e apenas o nome é diferente.
Quando se trata de promoção, se você tiver qualquer sobreposição com os nomes do campo do seu struct
, Go permite que você incorpore, mas a promoção do campo de sobreposição não acontece. Você ainda pode acessar o campo passando pelo caminho do nome do tipo.
Você não pode usar promoção ao inicializar structs
com tipos incorporados. Para inicializar os dados, você deve usar o nome dos tipos incorporados.
No código abaixo, definiremos algumas structs
e tipos personalizados. Vamos incorporar esses tipos em uma struct
:
package main
import (
"fmt"
)
type name string
type location struct {
x int
y int
}
type size struct {
width int
height int
}
type dot struct {
name
location
size
}
func getDots() []dot {
var dot1 dot
dot2 := dot{}
dot2.name = "A"
dot2.x = 5
dot2.y = 6
dot2.width = 10
dot2.height = 20
// When initializing embedded types, you can't use promotion. For name, the result is the same but for location and size, you need to put more work into this:
dot3 := dot{
name: "B",
location: location{
x: 13,
y: 27,
},
size: size{
width: 5,
height: 7,
},
}
dot4 := dot{}
dot4.name = "C"
dot4.location.x = 101
dot4.location.y = 209
dot4.size.width = 87
dot4.size.height = 43
return []dot{dot1, dot2, dot3, dot4}
}
func main() {
for key, dot := range getDots() {
fmt.Printf("dot%v: %#v\n", (key + 1), dot)
}
}
Executar o código anterior produz a seguinte saída:
dot1: main.dot{name:"", location:main.location{x:0, y:0}, size:main.size{width:0, height:0}}
dot2: main.dot{name:"A", location:main.location{x:5, y:6}, size:main.size{width:10, height:20}}
dot3: main.dot{name:"B", location:main.location{x:13, y:27}, size:main.size{width:5, height:7}}
dot4: main.dot{name:"C", location:main.location{x:101, y:209}, size:main.size{width:87, height:43}}
No exemplo de código acima, fomos capazes de definir uma struct
complexa incorporando outros tipos a ela. A incorporação (embedding) permite reutilizar structs
comuns, reduzindo o código duplicado, mas ainda fornecendo à struct
uma simples API.
Não veremos muita incorporação (embedding) no código Go nas aplicações. Ele aparece, mas a complexidade e a exceção significam que os desenvolvedores Go preferem ter os outros structs
como campos nomeados.