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
type Programmer struct {
Name string
Age int
Job string
Language []string
}Declaração
A declaração de structs é muito simples, exemplo abaixo
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
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
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.
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
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
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.
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.
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.
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
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
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
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
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.
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
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
`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.
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
type Num struct {
A int64
B int32
C int16
D int8
E int32
}Sabendo o número de bytes que estes tipos ocupam
int64ocupa 8 bytesint32ocupa 4 bytesint16ocupa 2 bytesint8ocupa 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
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
func main() {
type Empty struct {}
fmt.Println(unsafe.Sizeof(Empty{}))
}Saída
0Existem 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.
