Structs em Go são valores, então toda atribuição e toda chamada fazem uma cópia. Ponteiros deixam você compartilhar uma única struct, e receivers de ponteiro são o jeito de um método mudar o valor em que foi chamado.
Uma struct junta valores relacionados sob um nome, e os métodos dão comportamento a esse grupo. A maioria dos tipos reais em Go é struct, então é aqui que os seus próprios tipos começam.
O que faz as pessoas tropeçarem não é a sintaxe. É que uma struct é um valor, e Go copia valores sem cerimônia. Este post mostra onde as cópias acontecem, como os ponteiros as evitam e como isso decide que tipo de método escrever. Todo programa abaixo rodou no Go 1.26, e a saída foi colada da execução.
Definindo uma struct
Um tipo struct lista campos com nome, cada um com seu próprio tipo. Você constrói um valor dele com um literal composto:
package main
import "fmt"
type User struct {
Name string
Email string
Age int
}
func main() {
var zero User
ada := User{Name: "Ada", Age: 36}
bob := User{"Bob", "bob@example.com", 41}
fmt.Printf("%+v\n", zero)
fmt.Printf("%+v\n", ada)
fmt.Printf("%+v\n", bob)
ada.Email = "ada@example.com"
fmt.Println(ada.Name, ada.Email)
}
Ele imprime:
{Name: Email: Age:0}
{Name:Ada Email: Age:36}
{Name:Bob Email:bob@example.com Age:41}
Ada ada@example.com
var zero User dá uma struct com todos os campos no zero value: duas strings vazias e 0. Você nunca recebe uma struct pela metade, com lixo dentro.
ada usa campos com nome. Qualquer campo que você deixa de fora recebe o zero value, então Email é "". bob usa campos posicionais: os valores entram na ordem em que os campos foram declarados, e você precisa passar todos.
Prefira campos com nome. Com os posicionais, adicionar um campo a User quebra todo literal, e trocar dois campos string por engano compila sem um pio. O go vet até avisa sobre literais posicionais de tipos de outros pacotes. Você lê e escreve campos com um ponto, como em ada.Email.
Structs anônimas
Um tipo struct não precisa de nome. Para um formato usado uma vez só, você pode declarar e preencher tudo de uma vez:
package main
import "fmt"
func main() {
point := struct {
X, Y int
}{X: 3, Y: 4}
fmt.Printf("%+v\n", point)
}
Ele imprime:
{X:3 Y:4}
Você vai ver isso em testes, como tabela de casos, e para decodificar um pedaço de JSON que você só precisa uma vez. Qualquer coisa que você passa adiante merece um tipo com nome.
Structs são valores, e valores são copiados
Atribuir uma struct copia cada campo, e passá-la para uma função também:
package main
import "fmt"
type User struct {
Name string
Age int
}
func birthday(u User) {
u.Age++
fmt.Println("inside:", u.Age)
}
func main() {
ada := User{Name: "Ada", Age: 36}
copyOfAda := ada
copyOfAda.Name = "Not Ada"
birthday(ada)
fmt.Println(ada.Name, ada.Age)
fmt.Println(copyOfAda.Name)
}
Ele imprime:
inside: 37
Ada 36
Not Ada
copyOfAda é um segundo User, separado. Renomeá-lo não mexeu em ada. birthday também recebeu sua própria cópia, então somou um ano à cópia, imprimiu 37 e jogou a cópia fora ao retornar. O ada de quem chamou continua com 36.
É a mesma regra que você viu para arrays na parte sobre slices. Não é um caso especial das structs: em Go, toda atribuição e todo argumento é uma cópia. O que muda é o que é copiado. Numa struct, são todos os campos.
A mesma cópia se esconde num laço range, onde causa um bug silencioso:
package main
import "fmt"
type Player struct {
Name string
Score int
}
func main() {
team := []Player{{Name: "Ada", Score: 10}, {Name: "Bob", Score: 20}}
for _, p := range team {
p.Score += 5 // changes a copy
}
fmt.Println(team)
for i := range team {
team[i].Score += 5 // changes the element
}
fmt.Println(team)
}
Ele imprime:
[{Ada 10} {Bob 20}]
[{Ada 15} {Bob 25}]
No primeiro laço, p é uma cópia de cada elemento, então as pontuações não mudam. O segundo laço indexa o slice e muda os elementos de verdade.
Ponteiros: compartilhar em vez de copiar
Um ponteiro guarda o endereço de um valor, e isso deixa duas partes de um programa trabalharem na mesma struct. &x dá o endereço de x, e *p dá o valor para o qual p aponta:
package main
import "fmt"
type User struct {
Name string
Age int
}
func birthday(u *User) {
u.Age++
}
func main() {
ada := User{Name: "Ada", Age: 36}
p := &ada
fmt.Println((*p).Age, p.Age)
birthday(p)
birthday(&ada)
fmt.Println(ada.Age)
q := p
q.Name = "Ada L."
fmt.Println(ada.Name, p == q)
}
Ele imprime:
36 36
38
Ada L. true
O tipo *User quer dizer “ponteiro para um User“. p := &ada guarda o endereço de ada em p.
(*p).Age segue o ponteiro e lê o campo. Ninguém escreve assim, porque Go faz isso por você: p.Age num ponteiro para struct quer dizer exatamente a mesma coisa.
Agora birthday recebe um *User. Ela continua recebendo uma cópia do argumento, mas a cópia é um endereço, e os dois endereços levam a ada. Duas chamadas, dois aniversários, e ada.Age fica 38.
q := p copia o ponteiro, não a struct. Definir q.Name mudou ada, e p == q é true porque os dois ponteiros guardam o mesmo endereço.
Explicado como se você tivesse dez anos
Uma struct é uma casa. Um ponteiro é um papelzinho com o endereço da casa escrito nele.
Se você tira uma xerox do papel, fica com dois papéis. Não fica com duas casas. Os dois papéis levam à mesma porta, então se um amigo segue o papel dele e pinta a porta de vermelho, você encontra uma porta vermelha quando segue o seu.
Passar uma struct sem ponteiro é diferente. É como construir uma cópia da casa inteira, tijolo por tijolo, e entregar essa cópia. Seu amigo pode pintar aquela porta da cor que quiser. A sua casa não muda.
A versão precisa
Um valor de ponteiro é um endereço de memória, tipado pelo que ele aponta. Um *User tem o mesmo tamanho, uma palavra de máquina, não importa o tamanho de User. Copiar um ponteiro copia essa palavra, e depois as duas cópias se referem à mesma variável.
& pega o endereço de um valor endereçável: uma variável, um campo dela ou um elemento de slice. Um literal composto como &User{} também é permitido, como caso especial. * desreferencia. Para acessar campos e chamar métodos, Go desreferencia um ponteiro para struct automaticamente.
Onde a analogia falha: endereços de verdade podem ser calculados. A casa vizinha é um número acima. Go não tem aritmética de ponteiros, então você não consegue ir de um endereço para o próximo; só consegue seguir o ponteiro que recebeu. E uma casa de verdade pode ser demolida enquanto alguém ainda guarda o endereço dela. Em Go, o garbage collector mantém o valor vivo enquanto existir algum ponteiro para ele. É por isso que uma função pode retornar com segurança um ponteiro para uma de suas variáveis locais. A parte sobre memória explica onde esses valores moram.
new, e uma novidade do Go 1.26
new(T) aloca um zero value do tipo T e retorna um ponteiro para ele. A partir do Go 1.26, ele também aceita uma expressão, e dá um ponteiro para uma variável nova com esse valor:
package main
import "fmt"
type Config struct {
Retries int
Debug *bool
}
func main() {
n := new(int)
fmt.Println(*n)
*n = 7
fmt.Println(*n)
cfg := Config{Retries: 3, Debug: new(true)}
fmt.Println(cfg.Retries, *cfg.Debug)
}
Ele imprime:
0
7
3 true
new(int) aponta para um 0 novinho. new(true) é a forma nova. Antes do 1.26 você não podia escrever &true, então precisava de uma variável temporária ou de uma pequena função auxiliar só para conseguir um *bool. Campos opcionais como Debug aparecem muito em configuração e em JSON, onde um ponteiro separa “não definido” (nil) de false.
Para structs, você ainda vai escrever quase sempre &User{Name: "Ada"}. Faz o mesmo trabalho que new e preenche os campos ao mesmo tempo.
Ponteiros nil
O zero value de um ponteiro é nil, que quer dizer que ele não aponta para nada. Seguir um ponteiro nil para o programa:
package main
import "fmt"
type User struct {
Name string
}
func main() {
var p *User
fmt.Println(p == nil)
fmt.Println(p.Name)
}
Imprime a primeira linha e para:
true
panic: runtime error: invalid memory address or nil pointer dereference
p.Name na verdade é (*p).Name, e não existe *p para ler. Go não retorna um zero value aqui, e não lê uma memória qualquer. Ele entra em panic.
Esse é o panic mais comum em programas Go. Quando uma função pode retornar um ponteiro nil, confira antes de usar. O jeito comum é o padrão value, err: uma função que retorna um ponteiro nil também retorna um erro não nil, e você confere o erro primeiro.
Métodos
Um método é uma função com um receiver, o valor sobre o qual ela é chamada. O receiver vai entre parênteses antes do nome do método, e pode ser um valor ou um ponteiro. Essa escolha decide se o método consegue mudar alguma coisa:
package main
import "fmt"
type Counter struct {
count int
}
func (c Counter) IncValue() {
c.count++
}
func (c *Counter) IncPtr() {
c.count++
}
func main() {
var c Counter
c.IncValue()
fmt.Println("after IncValue:", c.count)
c.IncPtr()
fmt.Println("after IncPtr:", c.count)
}
Ele imprime:
after IncValue: 0
after IncPtr: 1
Os dois métodos têm a mesma linha, c.count++. Só um deles funcionou. Um receiver é só um parâmetro, então é copiado como qualquer outro. Veja o que cada método recebe de fato.
O programa do Counter acima. IncValue tem um receiver de valor, então trabalha numa cópia de c: o count da cópia vai para 1 e é jogado fora, e o programa imprime 0. IncPtr tem um receiver de ponteiro, então count++ passa pelo ponteiro e chega ao próprio c, e o programa imprime 1.
Aqui estão esses passos em palavras, caso a animação não rode para você:
var c Countercria um contador emmaincomcount0.c.IncValue()copiacpara o receiver.count++muda a cópia, então o count da cópia fica 1.- O método retorna e a cópia some.
ccontinua com count 0, e o programa imprimeafter IncValue: 0. c.IncPtr()dá ao método um ponteiro parac.count++segue o ponteiro e muda o próprioc.- O método retorna.
cfica com count 1, e o programa imprimeafter IncPtr: 1.
Um receiver de valor é o método dizendo “me dá uma xerox”. Um receiver de ponteiro diz “me dá o endereço”. Se um método precisa mudar o receiver, ele precisa de um receiver de ponteiro.
Go pega o endereço por você
Você chamou c.IncPtr() em c, um Counter comum, e não num *Counter. Funcionou porque Go reescreve a chamada por você:
package main
import "fmt"
type Counter struct {
count int
}
func (c *Counter) Inc() {
c.count++
}
func (c Counter) Get() int {
return c.count
}
func main() {
c := Counter{}
c.Inc() // Go writes (&c).Inc() for you
p := &c
p.Inc()
fmt.Println(p.Get()) // and (*p).Get() here
}
Ele imprime:
2
Quando c é uma variável e Inc quer um ponteiro, Go pega &c. Quando p é um ponteiro e Get quer um valor, Go usa *p. Então, no código do dia a dia, você chama métodos do mesmo jeito, seja qual for o receiver.
A reescrita só funciona quando existe um endereço para pegar. Um valor que não está guardado numa variável não tem endereço:
package main
type Counter struct {
count int
}
func (c *Counter) Inc() {
c.count++
}
func main() {
Counter{}.Inc()
}
O build falha com:
./main.go:12:12: cannot call pointer method Inc on Counter
Counter{} é um valor temporário, não uma variável, então não há nada para Inc apontar. Guarde-o numa variável primeiro, ou escreva (&Counter{}).Inc(). O mesmo limite é o motivo de você não conseguir chamar um método de ponteiro direto num elemento de map. Isso volta a importar na parte sobre interfaces, onde decide quais tipos satisfazem uma interface.
Quando usar um receiver de ponteiro
A regra prática é curta. Use um receiver de ponteiro quando qualquer uma destas for verdade:
- O método muda o receiver. Essa não tem discussão. Um receiver de valor muda uma cópia.
- A struct é grande. Um receiver de valor copia todos os campos a cada chamada. Um ponteiro é uma palavra. “Grande” não tem um limite exato; alguns campos pequenos funcionam bem como valor.
- O tipo não pode ser copiado com segurança. Uma struct que guarda um
sync.Mutex, por exemplo, não pode ser copiada. Ogo vetaponta cópias assim. A parte sobresyncexplica o porquê. - Outros métodos do tipo já usam receivers de ponteiro. Mantenha o tipo inteiro consistente: ou todos receivers de ponteiro, ou todos receivers de valor.
Receivers de valor combinam com tipos pequenos que se comportam como valores, como um Point ou uma quantia de Money, onde todo método só lê e uma cópia é barata. Na dúvida, use um receiver de ponteiro. É a escolha que a maior parte do código Go faz para structs.
Embedding: campos e métodos de outra struct
Go não tem herança. No lugar dela, uma struct pode fazer embedding de outro tipo, listando-o sem nome de campo, e os campos e métodos do tipo embutido são promovidos para a struct de fora:
package main
import "fmt"
type Address struct {
City string
}
func (a Address) Label() string {
return "lives in " + a.City
}
type Customer struct {
Name string
Address
}
func main() {
c := Customer{
Name: "Ada",
Address: Address{City: "London"},
}
fmt.Println(c.City)
fmt.Println(c.Label())
fmt.Println(c.Address.City)
}
Ele imprime:
London
lives in London
London
c.City e c.Label() funcionam como se o próprio Customer os declarasse. Por baixo, o valor embutido continua sendo um campo comum com o nome do seu tipo, Address, e é por isso que c.Address.City também funciona.
Isso é composição, não herança. Um Customer contém um Address; ele não é um. Você não pode passar um Customer onde uma função quer um Address. E quando Label roda, o receiver dele é o Address de dentro. Ele não sabe nada sobre o Customer em volta. Se Customer declarar seu próprio Label, esse vence, e o embutido continua lá como c.Address.Label().
Comparando structs, e struct tags
Você pode comparar duas structs com == quando todos os campos são comparáveis, e a comparação é campo a campo:
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
a := Point{X: 1, Y: 2}
b := Point{X: 1, Y: 2}
pa, pb := &a, &b
fmt.Println(a == b)
fmt.Println(pa == pb, *pa == *pb)
seen := map[Point]bool{a: true}
fmt.Println(seen[Point{1, 2}])
}
Ele imprime:
true
false true
true
a == b é verdadeiro porque os dois campos batem. Ponteiros, porém, comparam endereços. pa e pb apontam para duas variáveis diferentes, então pa == pb é falso, mesmo com os valores apontados sendo iguais. Structs comparáveis também servem como chaves de map, o que é útil para coordenadas e chaves compostas.
Adicione um campo que não pode ser comparado, como um slice, e o == para de compilar:
package main
import "fmt"
type Tagged struct {
Name string
Tags []string
}
func main() {
a := Tagged{Name: "x"}
b := Tagged{Name: "x"}
fmt.Println(a == b)
}
O build falha com:
./main.go:13:14: invalid operation: a == b (struct containing []string cannot be compared)
Slices, maps e funções não podem ser comparados com ==, e por isso uma struct que guarda um deles também não pode. Você compararia esses campos por conta própria, por exemplo com slices.Equal.
Campos também podem levar uma tag, uma string depois do tipo, como em Email string `json:"email"`. A linguagem ignora as tags. Os pacotes as leem, e encoding/json as usa para dar nome aos campos no JSON. As partes sobre construir uma API REST usam muito isso.
Construtores são só funções chamadas NewX
Go não tem construtores, então por convenção uma função chamada NewX constrói um X e retorna um ponteiro para ele:
package main
import (
"errors"
"fmt"
)
type Account struct {
Owner string
balance int
}
func NewAccount(owner string) (*Account, error) {
if owner == "" {
return nil, errors.New("owner is required")
}
return &Account{Owner: owner}, nil
}
func (a *Account) Deposit(amount int) {
a.balance += amount
}
func (a *Account) Balance() int {
return a.balance
}
func main() {
acc, err := NewAccount("Ada")
if err != nil {
fmt.Println(err)
return
}
acc.Deposit(50)
acc.Deposit(25)
fmt.Println(acc.Owner, acc.Balance())
_, err = NewAccount("")
fmt.Println(err)
}
Ele imprime:
Ada 75
owner is required
NewAccount confere a entrada antes de qualquer coisa existir, e retorna um erro em vez de uma conta quebrada. Ela retorna &Account{...}, o endereço de um valor que acabou de construir. Isso é seguro: como a analogia da casa disse, o valor fica vivo enquanto existir um ponteiro para ele.
Repare que balance começa com letra minúscula. Nomes em minúscula são privados do pacote, então código de outros pacotes precisa passar por Deposit e Balance. A parte sobre pacotes cobre essa regra. Todo método aqui tem receiver de ponteiro, inclusive Balance, que só lê, porque os outros métodos do tipo precisam de um.
Escreva um NewX quando uma struct precisa de validação ou de preparação. Quando o zero value já é útil, como o Counter acima, pule essa etapa e deixe as pessoas escreverem var c Counter.
O que lembrar
- Uma struct é um valor. Atribuir, passar como argumento e percorrer um slice delas com
range, tudo isso faz cópias. - Prefira campos com nome em literais de struct. Campos que faltam recebem o zero value.
- Um ponteiro guarda um endereço. Copiá-lo dá dois ponteiros para um só valor, e
p.Fieldsegue o ponteiro por você. - Um ponteiro nil não aponta para nada, e segui-lo causa panic.
- Um receiver de valor trabalha numa cópia. Se um método muda o receiver, ele precisa de um receiver de ponteiro, e os outros métodos do tipo normalmente devem acompanhar.
- Embedding promove campos e métodos. É composição, não herança.
- Uma função
NewXque retorna*Xé o construtor de Go, só por convenção.
Um receiver de valor recebe uma cópia; um receiver de ponteiro recebe a coisa de verdade.