Skip to content

Structs do Go

Structs podem armazenar um conjunto de dados de diferentes tipos, é um tipo composto. Go abandonou classes e herança, também abandonou métodos construtores, enfraqueceu deliberadamente as funcionalidades de orientação a objetos, Go não é uma linguagem OOP tradicional, mas ainda tem a sombra de OOP, através de structs e métodos também pode simular uma classe. Abaixo está um exemplo simples de struct

go
type Programmer struct {
  Name     string
  Age      int
  Job      string
  Language []string
}

Declaração

A declaração de structs é muito simples, exemplo abaixo

go
type Person struct {
   name string
   age int
}

O struct em si e seus campos internos seguem a convenção de nomenclatura com maiúsculas/minúsculas para exposição. Para campos adjacentes do mesmo tipo, não precisa declarar o tipo repetidamente, como abaixo

go
type Rectangle struct {
  height, width, area int
  color               string
}

TIP

Ao declarar campos de struct, o nome do campo não pode ser igual ao nome do método

Instanciação

Go não tem métodos construtores, na maioria dos casos usa-se a seguinte forma para instanciar structs, durante a inicialização é como map, especifica-se o nome do campo e depois inicializa o valor do campo

go
programmer := Programmer{
   Name:     "jack",
   Age:      19,
   Job:      "coder",
   Language: []string{"Go", "C++"},
}

Mas também pode omitir o nome do campo, quando omitir o nome do campo, deve inicializar todos os campos, geralmente não se recomenda usar esta forma, porque a legibilidade é ruim.

go
programmer := Programmer{
   "jack",
   19,
   "coder",
   []string{"Go", "C++"}}

Se o processo de instanciação for mais complexo, você também pode escrever uma função para instanciar o struct, como abaixo, você também pode considerá-lo como um construtor

go
type Person struct {
  Name    string
  Age     int
  Address string
  Salary  float64
}

func NewPerson(name string, age int, address string, salary float64) *Person {
  return &Person{Name: name, Age: age, Address: address, Salary: salary}
}

Mas Go não suporta sobrecarga de funções e métodos, então você não pode definir parâmetros diferentes para a mesma função ou método. Se quiser instanciar structs de múltiplas formas, ou cria múltiplos construtores, ou recomenda-se usar o padrão options.

Padrão Options

O padrão options é um padrão de projeto muito comum em Go, pode instanciar structs de forma mais flexível, com forte extensibilidade, e não precisa mudar a assinatura da função do construtor. Suponha que há o seguinte struct

go
type Person struct {
  Name     string
  Age      int
  Address  string
  Salary   float64
  Birthday string
}

Declara um tipo PersonOptions, ele aceita um parâmetro do tipo *Person, deve ser um ponteiro, porque queremos atribuir valores ao Person dentro de um closure.

go
type PersonOptions func(p *Person)

A seguir cria funções de opção, elas geralmente começam com With, o valor de retorno delas é uma função closure.

go
func WithName(name string) PersonOptions {
  return func(p *Person) {
    p.Name = name
  }
}

func WithAge(age int) PersonOptions {
  return func(p *Person) {
    p.Age = age
  }
}

func WithAddress(address string) PersonOptions {
  return func(p *Person) {
    p.Address = address
  }
}

func WithSalary(salary float64) PersonOptions {
  return func(p *Person) {
    p.Salary = salary
  }
}

A assinatura do construtor realmente declarado é a seguinte, ele aceita um parâmetro de comprimento variável do tipo PersonOptions.

go
func NewPerson(options ...PersonOptions) *Person {
    // aplica options primeiro
  p := &Person{}
    for _, option := range options {
        option(p)
    }

  // tratamento de valores padrão
  if p.Age < 0 {
    p.Age = 0
  }
  ......

    return p
}

Assim, para diferentes necessidades de instanciação, basta um construtor para completar, basta passar diferentes funções Options

go
func main() {
  pl := NewPerson(
    WithName("John Doe"),
    WithAge(25),
    WithAddress("123 Main St"),
    WithSalary(10000.00),
  )

  p2 := NewPerson(
    WithName("Mike jane"),
    WithAge(30),
  )
}

O padrão funcional de opções pode ser visto em muitos projetos open source, a instanciação do gRPC Server também usa este padrão de projeto. O padrão funcional de opções só é adequado para instanciação complexa, se houver apenas alguns parâmetros simples, recomenda-se usar um construtor comum para resolver.

Composição

Em Go, a relação entre structs é expressa através de composição, pode ser composição explícita ou composição anônima, esta última é usada mais como herança, mas essencialmente não há nenhuma mudança. Por exemplo

Forma de composição explícita

go
type Person struct {
   name string
   age  int
}

type Student struct {
   p      Person
   school string
}

type Employee struct {
   p   Person
   job string
}

Ao usar precisa especificar explicitamente o campo p

go
student := Student{
   p:      Person{name: "jack", age: 18},
   school: "lili school",
}
fmt.Println(student.p.name)

Já a composição anônima não precisa especificar explicitamente o campo

go
type Person struct {
  name string
  age  int
}

type Student struct {
  Person
  school string
}

type Employee struct {
  Person
  job string
}

O nome do campo anônimo é por padrão o nome do tipo, o chamador pode acessar diretamente os campos e métodos desse tipo, mas além de ser mais conveniente, não há nenhuma diferença da primeira forma.

go
student := Student{
   Person: Person{name: "jack",age: 18},
   school: "lili school",
}
fmt.Println(student.name)

Ponteiros

Para ponteiros de struct, não precisa desreferenciar para acessar diretamente o conteúdo do struct, exemplo abaixo

go
p := &Person{
   name: "jack",
   age:  18,
}
fmt.Println(p.age,p.name)

Durante a compilação será convertido para (*p).name, (*p).age, na verdade ainda precisa desreferenciar, mas ao codificar pode omitir, é uma espécie de açúcar sintático.

Tags

Tags de struct são uma forma de metaprogramação, combinadas com reflexão podem fazer muitas funcionalidades incríveis, o formato é o seguinte

go
`key1:"val1" key2:"val2"`

A tag é uma forma de par chave-valor, usa espaço para separar. A tolerância a erros das tags de struct é muito baixa, se não escrever a tag corretamente, não será possível ler normalmente, mas na compilação não haverá nenhum erro, abaixo está um exemplo de uso.

go
type Programmer struct {
    Name     string `json:"name"`
    Age      int `yaml:"age"`
    Job      string `toml:"job"`
    Language []string `properties:"language"`
}

A aplicação mais ampla de tags de struct é na definição de alias em vários formatos de serialização, o uso de tags precisa ser combinado com reflexão para aproveitar completamente sua funcionalidade.

Alinhamento de Memória

A distribuição de memória dos campos de struct em Go segue as regras de alinhamento de memória, fazer isso pode reduzir o número de vezes que a CPU acessa a memória, correspondentemente ocupa mais memória, é um meio de trocar espaço por tempo. Suponha o seguinte struct

go
type Num struct {
  A int64
  B int32
  C int16
  D int8
    E int32
}

Sabendo o número de bytes que estes tipos ocupam

  • int64 ocupa 8 bytes
  • int32 ocupa 4 bytes
  • int16 ocupa 2 bytes
  • int8 ocupa 1 byte

O uso de memória de todo o struct parece ser 8+4+2+1+4=19 bytes? Claro que não, segundo as regras de alinhamento de memória, o uso de memória do struct é pelo menos um múltiplo inteiro do maior campo, o que faltar é completado. O maior campo neste struct é int64 ocupando 8 bytes, então a distribuição de memória é como mostrado no diagrama abaixo

Então na verdade ocupa 24 bytes, dos quais 5 bytes são inúteis.

Veja este outro struct abaixo

go
type Num struct {
  A int8
  B int64
  C int8
}

Entendendo as regras acima, pode-se entender rapidamente que seu uso de memória também é 24 bytes, embora tenha apenas três campos, desperdiçando 14 bytes inteiros.

Mas podemos ajustar os campos, mudando para a seguinte ordem

type Num struct {
  A int8
  C int8
  B int64
}

Assim o uso de memória se torna 16 bytes, desperdiçando 6 bytes, reduzindo o desperdício de 8 bytes.

Teoricamente, fazer os campos do struct se distribuírem em uma ordem razoável pode reduzir seu uso de memória. Mas no processo real de codificação, não há razão necessária para fazer isso, não necessariamente trará melhorias substanciais na redução do uso de memória, mas certamente vai aumentar a pressão arterial e a carga mental dos desenvolvedores, especialmente em negócios onde alguns structs podem ter dezenas ou centenas de campos, então apenas entenda o conceito.

TIP

Se você realmente quiser economizar memória através deste método, pode dar uma olhada nestas duas bibliotecas

Eles vão verificar os structs no seu código fonte, calcular e reorganizar os campos do struct para minimizar o uso de memória do struct.

Struct Vazio

Um struct vazio não tem campos, não ocupa espaço de memória, podemos usar a função unsafe.SizeOf para calcular o tamanho em bytes ocupado

go
func main() {
   type Empty struct {}
   fmt.Println(unsafe.Sizeof(Empty{}))
}

Saída

0

Existem muitos cenários de uso para structs vazios, como mencionado anteriormente, como tipo de valor de map, pode usar map como set, ou como tipo de canal, representando um canal apenas para notificação.

Golang por www.golangdev.cn edit