1 Fundamentos de Struct
Na linguagem Go, um struct é um tipo de dado composto usado para agregar diferentes tipos de dados em uma única entidade. Os structs ocupam uma posição significativa em Go, pois servem como um aspecto fundamental da programação orientada a objetos, embora com pequenas diferenças em relação às linguagens de programação orientada a objetos tradicionais.
A necessidade de structs surge pelos seguintes aspectos:
- Organizar variáveis com forte relevância juntas para aprimorar a manutenção do código.
- Fornecer um meio para simular "classes", facilitando recursos de encapsulamento e agregação.
- Ao interagir com estruturas de dados como JSON, registros de banco de dados, etc., os structs oferecem uma ferramenta de mapeamento conveniente.
Organizar dados com structs permite uma representação mais clara de modelos de objetos do mundo real, como usuários, pedidos, etc.
2 Definindo um Struct
A sintaxe para definir um struct é a seguinte:
type NomeStruct struct {
Campo1 TipoCampo1
Campo2 TipoCampo2
// ... outras variáveis membros
}
- A palavra-chave
type
introduz a definição do struct. -
NomeStruct
é o nome do tipo de struct, seguindo as convenções de nomenclatura de Go, geralmente é capitalizado para indicar sua exportabilidade. - A palavra-chave
struct
significa que este é um tipo struct. - Dentro das chaves
{}
, as variáveis membros (campos) do struct são definidas, sendo cada uma seguida pelo seu tipo.
O tipo dos membros do struct pode ser qualquer tipo, incluindo tipos básicos (como int
, string
, etc.) e tipos complexos (como arrays, slices, outro struct, etc.).
Por exemplo, a definição de um struct representando uma pessoa:
type Pessoa struct {
Nome string
Idade int
Emails []string // pode incluir tipos complexos, como slices
}
No código acima, o struct Pessoa
tem três variáveis membros: Nome
do tipo string, Idade
do tipo inteiro e Emails
do tipo slice de string, indicando que uma pessoa pode ter vários endereços de e-mail.
3 Criando e Inicializando um Struct
3.1 Criando uma Instância de Struct
Há duas maneiras de criar uma instância de struct: declaração direta ou usando a palavra-chave new
.
Declaração direta:
var p Pessoa
O código acima cria uma instância p
do tipo Pessoa
, onde cada variável membro do struct é o valor zero do seu tipo correspondente.
Usando a palavra-chave new
:
p := new(Pessoa)
Criar um struct usando a palavra-chave new
resulta em um ponteiro para o struct. A variável p
neste ponto é do tipo *Pessoa
, apontando para uma variável recém-alocada do tipo Pessoa
onde as variáveis membros foram inicializadas com valores zero.
3.2 Inicializando Instâncias de Struct
As instâncias de struct podem ser inicializadas de uma vez ao serem criadas, usando dois métodos: com nomes de campos ou sem nomes de campos.
Inicializando com Nomes de Campos:
p := Pessoa{
Nome: "Alice",
Idade: 30,
Emails: []string{"[email protected]", "[email protected]"},
}
Ao inicializar com a forma de atribuição de campo, a ordem de inicialização não precisa ser a mesma que a ordem de declaração do struct, e quaisquer campos não inicializados manterão seus valores zero.
Inicializando sem Nomes de Campos:
p := Pessoa{"Bob", 25, []string{"[email protected]"}}
Ao inicializar sem nomes de campos, certifique-se de que os valores iniciais de cada variável membro estejam na mesma ordem que quando o struct foi definido, e nenhum campo pode ser omitido.
Além disso, os structs podem ser inicializados com campos específicos, e quaisquer campos não especificados assumirão os valores zero:
p := Pessoa{Nome: "Charlie"}
Neste exemplo, apenas o campo Nome
é inicializado, enquanto Idade
e Emails
assumirão seus valores zero correspondentes.
4 Acessando Membros de Struct
Acessar as variáveis membros de um struct em Go é muito direto, alcançado usando o operador ponto (.
). Se você tem uma variável struct, pode ler ou modificar seus valores membros dessa maneira.
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
// Criar uma variável do tipo Person
p := Person{"Alice", 30}
// Acessar os membros da struct
fmt.Println("Nome:", p.Name)
fmt.Println("Idade:", p.Age)
// Modificar os valores dos membros
p.Name = "Bob"
p.Age = 25
// Acessar os valores dos membros modificados novamente
fmt.Println("\nNome Atualizado:", p.Name)
fmt.Println("Idade Atualizada:", p.Age)
}
Neste exemplo, primeiro definimos uma struct Person
com duas variáveis de membro, Name
e Age
. Em seguida, criamos uma instância dessa struct e demonstramos como ler e modificar esses membros.
5 Composição e Aninhamento de Structs
Structs não apenas podem existir independentemente, mas também podem ser compostos e aninhados juntos para criar estruturas de dados mais complexas.
5.1 Structs Anônimas
Uma struct anônima não declara explicitamente um novo tipo, mas usa diretamente a definição da struct. Isso é útil quando você precisa criar uma struct uma vez e usá-la de forma simples, evitando a criação de tipos desnecessários.
Exemplo:
package main
import "fmt"
func main() {
// Definir e inicializar uma struct anônima
pessoa := struct {
Nome string
Idade int
}{
Nome: "Eva",
Idade: 40,
}
// Acessar os membros da struct anônima
fmt.Println("Nome:", pessoa.Nome)
fmt.Println("Idade:", pessoa.Idade)
}
Neste exemplo, em vez de criar um novo tipo, definimos diretamente uma struct e criamos uma instância dela. Este exemplo demonstra como inicializar uma struct anônima e acessar seus membros.
5.2 Aninhamento de Structs
O aninhamento de structs envolve a inclusão de uma struct como membro de outra struct. Isso nos permite construir modelos de dados mais complexos.
Exemplo:
package main
import "fmt"
// Definir a struct Address
type Endereco struct {
Cidade string
País string
}
// Incluir a struct Address na struct Person
type Pessoa struct {
Nome string
Idade int
Endereco Endereco
}
func main() {
// Inicializar uma instância de Pessoa
p := Pessoa{
Nome: "Carlos",
Idade: 28,
Endereco: Endereco{
Cidade: "Nova Iorque",
País: "EUA",
},
}
// Acessar os membros da struct aninhada
fmt.Println("Nome:", p.Nome)
fmt.Println("Idade:", p.Idade)
// Acessar os membros da struct Address
fmt.Println("Cidade:", p.Endereco.Cidade)
fmt.Println("País:", p.Endereco.País)
}
Neste exemplo, definimos uma struct Endereco
e a incluímos como membro na struct Pessoa
. Ao criar uma instância da Pessoa
, também criamos uma instância de Endereco
simultaneamente. Podemos acessar os membros da struct aninhada usando a notação de ponto.
6 Métodos de Struct
Recursos de programação orientada a objetos (OOP) podem ser implementados por meio de métodos de struct.
6.1 Conceitos Básicos de Métodos
Na linguagem Go, embora não haja um conceito tradicional de classes e objetos, recursos semelhantes à OOP podem ser alcançados vinculando métodos a structs. Um método de struct é um tipo especial de função associada a um tipo específico de struct (ou a um ponteiro para uma struct), permitindo que esse tipo tenha seu próprio conjunto de métodos.
// Definir uma struct simples
type Retangulo struct {
comprimento, largura float64
}
// Definir um método para a struct Retangulo para calcular a área do retângulo
func (r Retangulo) Area() float64 {
return r.comprimento * r.largura
}
No código acima, o método Area
está associado à struct Retangulo
. Na definição do método, (r Retangulo)
é o receptor, que especifica que este método está associado ao tipo Retangulo
. O receptor aparece antes do nome do método.
6.2 Receptores de Valor e Ponteiro
Os métodos podem ser categorizados como receptores de valor e receptores de ponteiro com base no tipo de receptor. Receptores de valor usam uma cópia da struct para chamar o método, enquanto receptores de ponteiro usam um ponteiro para a struct e podem modificar a struct original.
// Definir um método com um receptor de valor
func (r Retângulo) Perímetro() float64 {
return 2 * (r.comprimento + r.largura)
}
// Definir um método com um receptor de ponteiro, que pode modificar a struct
func (r *Retângulo) DefinirComprimento(novoComprimento float64) {
r.comprimento = novoComprimento // pode modificar o valor original da struct
}
No exemplo acima, Perímetro
é um método com receptor de valor, chamá-lo não alterará o valor do Retângulo
. No entanto, DefinirComprimento
é um método com receptor de ponteiro, e chamá-lo afetará a instância original de Retângulo
.
6.3 Invocação de Método
Você pode chamar métodos de uma struct usando a variável da struct e seu ponteiro.
func main() {
ret := Retângulo{comprimento: 10, largura: 5}
// Chamar o método com receptor de valor
fmt.Println("Área:", ret.Área())
// Chamar o método com receptor de valor
fmt.Println("Perímetro:", ret.Perímetro())
// Chamar o método com receptor de ponteiro
ret.DefinirComprimento(20)
// Chamar o método com receptor de valor novamente, observe que o comprimento foi modificado
fmt.Println("Após modificação, Área:", ret.Área())
}
Ao chamar um método usando um ponteiro, o Go manipula automaticamente a conversão entre valores e ponteiros, independentemente de o método ser definido com um receptor de valor ou um receptor de ponteiro.
6.4 Seleção do Tipo de Receptor
Ao definir métodos, você deve decidir se usar um receptor de valor ou um receptor de ponteiro com base na situação. Aqui estão algumas diretrizes comuns:
- Se o método precisa modificar o conteúdo da estrutura, use um receptor de ponteiro.
- Se a estrutura for grande e o custo da cópia for alto, use um receptor de ponteiro.
- Se você deseja que o método modifique o valor para o qual o receptor aponta, use um receptor de ponteiro.
- Por motivos de eficiência, mesmo que você não modifique o conteúdo da estrutura, é razoável usar um receptor de ponteiro para uma estrutura grande.
- Para estruturas pequenas, ou quando apenas for necessário ler dados sem a necessidade de modificação, um receptor de valor é frequentemente mais simples e eficiente.
Através de métodos de struct, podemos simular algumas características da programação orientada a objetos em Go, como encapsulamento e métodos. Essa abordagem em Go simplifica o conceito de objetos, proporcionando capacidade suficiente para organizar e gerenciar funções relacionadas.
7 Struct e Serialização JSON
Em Go, muitas vezes é necessário serializar uma struct para o formato JSON para transmissão em rede ou como arquivo de configuração. Da mesma forma, também precisamos ser capazes de desserializar o JSON em instâncias de struct. O pacote encoding/json
em Go fornece essa funcionalidade.
Aqui está um exemplo de como converter entre uma struct e JSON:
package main
import (
"encoding/json"
"fmt"
"log"
)
// Definir a struct Pessoa e usar tags json para definir a correspondência entre os campos da struct e os nomes dos campos JSON
type Pessoa struct {
Nome string `json:"nome"`
Idade int `json:"idade"`
Emails []string `json:"emails,omitempty"`
}
func main() {
// Criar uma nova instância de Pessoa
p := Pessoa{
Nome: "Fulano de Tal",
Idade: 30,
Emails: []string{"[email protected]", "[email protected]"},
}
// Serializar para JSON
dadosJSON, err := json.Marshal(p)
if err != nil {
log.Fatalf("Falha na serialização JSON: %s", err)
}
fmt.Printf("Formato JSON: %s\n", dadosJSON)
// Desserializar em uma struct
var p2 Pessoa
if err := json.Unmarshal(dadosJSON, &p2); err != nil {
log.Fatalf("Falha na desserialização JSON: %s", err)
}
fmt.Printf("Struct Recuperada: %#v\n", p2)
}
No código acima, definimos uma estrutura Pessoa
, incluindo um campo do tipo slice com a opção "omitempty". Essa opção especifica que se o campo estiver vazio ou ausente, ele não será incluído no JSON.
Usamos a função json.Marshal
para serializar uma instância de struct em JSON, e a função json.Unmarshal
para desserializar dados JSON em uma instância de struct.
8 Tópicos Avançados em Structs
8.1 Comparação de Structs
Em Go, é permitido comparar diretamente duas instâncias de structs, mas essa comparação é baseada nos valores dos campos dentro das structs. Se todos os valores dos campos forem iguais, então as duas instâncias das structs são consideradas iguais. Deve-se notar que nem todos os tipos de campos podem ser comparados. Por exemplo, uma struct contendo slices não pode ser comparada diretamente.
Aqui está um exemplo de comparação de structs:
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{1, 3}
fmt.Println("p1 == p2:", p1 == p2) // Saída: p1 == p2: true
fmt.Println("p1 == p3:", p1 == p3) // Saída: p1 == p3: false
}
Neste exemplo, p1
e p2
são consideradas iguais porque todos os seus valores de campos são iguais. E p3
não é igual a p1
porque o valor de Y
é diferente.
8.2 Copiando Structs
Em Go, instâncias de structs podem ser copiadas por atribuição. Se esta cópia é uma cópia profunda ou uma cópia rasa depende dos tipos dos campos dentro da struct.
Se a struct contém apenas tipos básicos (como int
, string
, etc.), a cópia é uma cópia profunda. Se a struct contém tipos de referência (como slices, maps, etc.), a cópia será uma cópia rasa, e a instância original e a instância copiada compartilharão a memória dos tipos de referência.
Aqui está um exemplo de cópia de uma struct:
package main
import "fmt"
type Data struct {
Numbers []int
}
func main() {
// Inicialize uma instância da struct Data
original := Data{Numbers: []int{1, 2, 3}}
// Copie a struct
copied := original
// Modifique os elementos da slice copiada
copied.Numbers[0] = 100
// Veja os elementos das instâncias original e copiada
fmt.Println("Original:", original.Numbers) // Saída: Original: [100 2 3]
fmt.Println("Copiada:", copied.Numbers) // Saída: Copiada: [100 2 3]
}
Como mostrado no exemplo, as instâncias original
e copied
compartilham a mesma slice, então modificar os dados da slice em copied
também afetará os dados da slice em original
.
Para evitar esse problema, você pode obter uma verdadeira cópia profunda ao copiar explicitamente o conteúdo da slice para uma nova slice:
novosNumeros := make([]int, len(original.Numbers))
copy(novosNumeros, original.Numbers)
copied := Data{Numbers: newNumbers}
Dessa forma, quaisquer modificações em copied
não afetarão original
.