Skip to content

Structs

In Go, structs are composite data types used to group together variables (fields) under a single data structure. They are similar to classes in other languages, but Go doesn't have classes in the traditional sense. You define a struct using the type keyword, followed by the name of the struct and a set of field declarations within curly braces. To create an instance of a struct, you can use the struct's name followed by curly braces and provide values for its fields.

After declaring a struct type, you can define variables of that type either using the var keyword or employing the := syntax. In the absence of a value assigned to the variable, it assumes the zero value corresponding to the struct type. A zero-value struct entails each field being set to its respective zero value. There is no difference between assigning an empty struct literal and not assigning a value at all. Both scenarios result in initializing all fields within the struct to their zero values.

Initialization and Zero-Values for Structs

run command
$ go run src/structs/basic.go
{ 0}
{ 0}
Both empty structs are equal?  true
Create object person, from Person struct: {Gabriel 100}
Created using field order: {Gabriel2 200}
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    var emptyPerson Person
    emptyPerson2 := Person{}

    fmt.Println(emptyPerson)
    fmt.Println(emptyPerson2)
    fmt.Println("Both empty structs are equal? ", emptyPerson == emptyPerson2)

    person := Person{
        Name: "Gabriel",
        Age:  100,
    }
    fmt.Printf("Created using field names: %v\n", person)

    person2 := Person{
        "Gabriel2",
        200,
    }
    fmt.Printf("Created using field order: %v\n", person2)
}

Anonymous Structs

Anonymous Structs are structs without a defined name, often used for temporary data structures. Anonymous structs prove useful in a two typical scenarios. Firstly, when translating external data into a struct or vice versa, such as dealing with JSON or Protocol Buffers. This process is commonly referred to as unmarshaling and marshaling data, respectively.

Using Anonymous Structs

run command
$ go run src/structs/anonymous.go
Vector: {10 20}
X : 10
Y : 20
Person: {Yuri Alberto 123}
Name : Yuri Alberto
Age : 123
package main

import "fmt"

func main() {
    vector := struct {
        X int
        Y int
    }{
        X: 10,
        Y: 20,
    }
    fmt.Printf("Vector: %v\n", vector)
    fmt.Printf("X : %v\n", vector.X)
    fmt.Printf("Y : %v\n", vector.Y)

    var person struct {
        name string
        age  int
    }
    person.name = "Yuri Alberto"
    person.age = 123
    fmt.Printf("Person: %v\n", person)
    fmt.Printf("Name : %v\n", person.name)
    fmt.Printf("Age : %v\n", person.age)

}

Compare Structs

The comparability of a struct depends on its fields. Structs composed entirely of comparable types are comparable, while those containing slice, map, function, and channel fields are not.

Go lacks a mechanism that allow overriding to redefine equality, thereby enabling the use of == and != with incomparable structs. However, developers have the option to craft custom functions for comparing structs as an alternative approach.

Composing Structs

Go doesn't support inheritance since the language encourages code reuse by composition and promotion. Structs can have embedded fields, that's fields without names assigned. Any fields or methods declared on an embedded field are promoted to the containing struct and can be invoked directly. Any type can be embedded.

For fields or methods with the same name between structs, it's necessary to use the embedded field's name.

Compose Structs and Consuming

run command
$ go run src/structs/composing.go
Person Name: Gabriel, Age: 20, and Team: Corinthians
Team Name: Corinthians, Location: São Paulo
São Paulo
package main

import "fmt"

type Team struct {
    Name     string
    Location string
}

func (t Team) Print() string {
    return fmt.Sprintf("Team Name: %s, Location: %s", t.Name, t.Location)
}

type Person struct {
    Name string
    Age  int
    Team
}

func (p Person) Print() string {
    return fmt.Sprintf("Person Name: %s, Age: %d, and Team: %s", p.Name, p.Age, p.Team.Name)
}

func main() {
    person := Person{
        Name: "Gabriel",
        Age:  20,
        Team: Team{Name: "Corinthians", Location: "São Paulo"},
    }

    fmt.Println(person.Print())
    fmt.Println(person.Team.Print()) // Using field Team to access Team's print struct method
    fmt.Println(person.Location)     // Using Location field DIRECTLY
}

Warning

Embedding is not inheritance, it's a different feature that Go supports.

Struct Methods

In Go, structs can have methods associated with them. These methods are functions that operate on instances of the struct, and they enable you to define behavior specific to the struct type.

Methods can be defined just at the package block level, while functions can be defined inside any block. Methods names cannot be overloaded also, and the methods must be declared in the same package as their associated type, it's not possible to add methods to types you don't control.

Simple Example of Methods

run command
$ go run structs/methods.go
Area: 78.5
package main

import "fmt"

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

func main() {
    circle := Circle{Radius: 5.0}
    area := circle.Area()
    fmt.Printf("Area: %v\n", area)
}

Pointer Receivers vs Value Receivers

Check out: Use Pointers with Wisdom.

Pointer receivers can modify the state of the struct directly. Using a pointer receiver allows you to change the state of an instance directly. When using value receivers, you're essentially passing a copy of the struct. Using pointers, you can pass a reference to the struct, allowing the function to modify the original data.

Using Pointer and Value Receivers

run command
$ go run structs/methods_pointer.go
Area: 78.5
Circle change? {5}
Area: 78.5
Circle change? {100}
package main

import "fmt"

type Circle struct {
    Radius float64
}

func (c Circle) NormalArea() float64 {
    result := 3.14 * c.Radius * c.Radius
    c.Radius = 100
    return result
}

func (c *Circle) PointerArea() float64 {
    result := 3.14 * c.Radius * c.Radius
    c.Radius = 100
    return result
}

func main() {
    circle := Circle{Radius: 5.0}
    area := circle.NormalArea()
    fmt.Printf("Area: %v\n", area)
    fmt.Printf("Circle change? %v\n", circle)
    area = circle.PointerArea()
    fmt.Printf("Area: %v\n", area)
    fmt.Printf("Circle change? %v\n", circle)
}

Pointers Constructors

In Go, when you create a constructor function for a struct, it typically returns a reference to the newly created struct. With this technique, any modification made to the reference returned by the constructor will affect all places that use that reference. This can be quite powerful, especially when managing resources like database connections.

A Simple Pointer Constructor

run command
$ go run structs/constructor_pointer.go
Query 1 executed **nice connection!*** true
Query 2 executed **nice connection!*** true
Query 3 executed **nice connection!*** true
Closing the DB conncetion  **nice connection!***
Query 1 executed **nice connection!*** true
Query 2 executed **nice connection!*** false
Query 3 executed **nice connection!*** false
package main

import "fmt"

type DbConnection struct {
    isOpen   bool
    str_conn string
}

func NewDbConnection(str_conn string) *DbConnection {
    return &DbConnection{
        isOpen:   true,
        str_conn: str_conn,
    }
}

func (conn *DbConnection) Close() {
    conn.isOpen = false
    fmt.Println("Closing the DB conncetion ", conn.str_conn)
}

func main() {
    dbConn := NewDbConnection("**nice connection!***")
    dbConnValue := *dbConn
    performQuery1(dbConnValue)
    performQuery2(dbConn)
    performQuery3(dbConn)

    dbConn.Close()

    performQuery1(dbConnValue)
    performQuery2(dbConn)
    performQuery3(dbConn)
}

func performQuery1(conn DbConnection) {
    fmt.Println("Query 1 executed", conn.str_conn, conn.isOpen)
}

func performQuery2(conn *DbConnection) {
    fmt.Println("Query 2 executed", conn.str_conn, conn.isOpen)
}

func performQuery3(conn *DbConnection) {
    fmt.Println("Query 3 executed", conn.str_conn, conn.isOpen)
}

Structs Tags

Struct tags are annotations that can be added to the fields of a struct to provide additional information or metadata about the field. Struct tags are typically used to describe how the struct fields should be encoded or decoded to and from other formats, such as JSON, XML, or other data serialization formats. They play a crucial role in mapping Go data structures to external formats, enabling seamless data interchange between different systems or languages.

Example
  1. Struct tags are added to each field to specify the corresponding JSON field names. For example, the json:"name" tag indicates that the Name field should be encoded as "name" in JSON.
  2. The omitempty option in the json:"address,omitempty" tag indicates that the Address field should be omitted from the JSON output if it is empty.

run command
go run src/structs/tags.go
package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name    string `json:"name"`
    Age     int    `json:"age"`
    Address string `json:"address,omitempty"`
}

func main() {
    person := Person{Name: "Gabriel", Age: 25}
    jsonData, err := json.Marshal(person)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("JSON Data:", string(jsonData))
}
output
JSON Data: {"name":"Gabriel","age":25}