Struct

A struct is a collection of fields of different types that logically belong together, that creates a new type. It's not a reference type, which means, when a struct passed to a function it will be copied. struct's fields will hold their default values until it gets defined.

General syntax:

type [NAME] struct {
	[FIELD NAME] [FIELD TYPE] [TAGS...]
}

Here's an example of creating a struct:

type Vertex struct {
	X int `json:"x"`
  Y int `json:"y"`
	Z int `json:"z"`
}

The above syntax can be further simplified if tags are not defined:

type Vertex struct {
	X, Y, Z int
}

Fields' values can be modified later:

v := Vertex{
	X: 1,
	Y: 2,
	Z: 3,
}
 
v.X = 3
fmt.Println(v)

Pointers to the struct can also be created using the & operator:

v := Vertex{
	X: 1,
	Y: 2,
	Z: 3,
}
 
p := &v
 
fmt.Println(p)

A new variable from a struct can be created in various ways:

fmt.Println(Vertex{1, 2, 3})
fmt.Println(Vertex{X: 1})
fmt.Println(Vertex{Y: 2, Z: 3})
 
// pointer
fmt.Println(&Vertex{1, 2, 3})

For accessing different fields or modifying them, you can use the . character. This also works with pointers, but you need to check that the pointer is not nil before reading it.

v := Vertex{
	X: 1,
	Y: 2,
	Z: 3,
}
 
fmt.Println(v.X)
 
p := &v
fmt.Println(p.X)

Play with the following example in your browser here (opens in a new tab)

package main
 
import "fmt"
 
type Vertex struct {
	X int `json:"x"`
	Y int `json:"y"`
	Z int `json:"z"`
}
 
func main() {
	fmt.Println(Vertex{1, 2, 3})
	fmt.Println(Vertex{X: 1})
	fmt.Println(Vertex{Y: 2, Z: 3})
 
	v := &Vertex{1, 2, 3}
 
	// pointer
	fmt.Println(&Vertex{1, 2, 3})
 
	fmt.Println("X", v.X)
}

Anonymous Struct

In Go, an anonymous struct is a struct type that can be defined without a name, without even using the type keyword. It can be created directly in a variable declaration or function call.

In the following code snippet, a variable named point receives an anonymous struct object with two integer fields, x and y. Afterward, the value of the x field is modified just as it would be with a regular named struct.

package main
 
import "fmt"
 
func main() {
	point := struct {
		x, y int
	}{
		x: 10,
		y: 20,
	}
 
	fmt.Println(point)
 
	point.x = 30
 
	fmt.Println(point)
}

You can run the above code here (opens in a new tab).

In the next example, a traditional named struct and an anonymous struct are compared. Both have identical field names and values. The comparison evaluates to true.

package main
 
import "fmt"
 
type Point struct {
	x, y int
}
 
func main() {
	point := struct {
		x, y int
	}{
		x: 10,
		y: 20,
	}
 
	p2 := Point{
		x: 10,
		y: 20,
	}
 
	fmt.Println(point == p2) // true
}

You can run the above code snippet here (opens in a new tab).

Empty struct

An empty struct (struct) is a special type that takes up zero bytes of storage. It is often used when the only information needed is the existence of an item, rather than any additional data. This makes it a highly efficient way to represent concepts like presence, membership, or signaling.

Example for checking for memberships

set := make(map[string]struct{})
set["apple"] = struct{}{}
set["banana"] = struct{}{}
 
// Check for membership
if _, exists := set["apple"]; exists {
    fmt.Println("Apple is in the set.")
}

Example for channel signals

done := make(chan struct{})
 
// Goroutine
go func() {
    // Do some work
    done <- struct{}{} // Signal completion
}()
 
<-done // Wait for signal

Struct Embedding

struct embedding is a mechanism that allows you to compose or embed one struct type within another. It allows you to achieve code reuse and create more complex data structures by building on existing ones.

The fields and methods of the embedded struct become part of the embedding struct. This enables you to access the embedded struct's fields and methods as if they were directly defined in the embedding struct.

type Base struct {
	ID   int
	Name string
}
 
func (b Base) GetName() string {
	return b.Name
}
 
type Log struct {
	CreationID int
	UpdateID   int
}
 
func (l Log) GetCreationID() int {
	return l.CreationID
}
 
func (l Log) GetUpdateID() int {
	return l.UpdateID
}
 
type User struct {
	Base
	Log
}

It's important to note that, with type embedding, the methods of the embedded type become methods of the outer type, but when invoked, the receiver will be the inner type, not the outer type. So the User type will directly have the GetName function because it embeds the Base type. When the GetName method is called, the receiver will be the Base type, not the User type. Unless the User type overrides.

It's possible to shadow methods of the inner type to access them using the receiver of the outer type.

func (u User) GetName() string {
    return u.Base.Name
}

When creating an instance of the outer type, you can set the values of the inner types:

func NewUser(name string, creationID int) User {
	return User{
		Base{Name: name},
		Log{CreationID: creationID},
	}
}

But this is not mandatory; Otherwise the fields will take their default values.

func NewUser(name string, creationID int) User {
	return User{}
}

Embedded types introduce naming conflicts, but these can be resolved easily. The fields or methods of the outer type always shadow those of the inner type. Additionally, the name of the embedded can't be the same to the field name of the enclosing type at the same level.

type User struct {
	Log
	Log Logger
}

Without the ability to embed types, you would need to write enclosing functions around the fields, just like in the below example, which would increase the lines of code without substantially improving functionality.

type User struct {
	base Base
	log  Log
}
 
func (u User) GetName() string {
	return u.base.Name
}

Difference between embedding and inheritance

Main difference between embedding and inheritance is that the inner type can't reference the outer type in a method that got defined at the inner type.

Struct Memory Layout

The memory layout of a struct depends on the order of its fields. The compiler automatically inserts padding to satisfy alignment requirements.

Optimizing Struct Layout

Reordering fields in a struct can minimize padding and reduce memory usage. Fields are grouped to minimize padding, resulting in better memory utilization.

package main
 
import (
	"fmt"
	"unsafe"
)
 
type Unoptimized struct {
	A int8
	B int64
	C int8
}
 
type Optimized struct {
	B int64
	A int8
	C int8
}
 
func main() {
	fmt.Println("Size of Unoptimized struct:", unsafe.Sizeof(Unoptimized{})) // 24 bytes
	fmt.Println("Size of Optimized struct:", unsafe.Sizeof(Optimized{}))     // 16 bytes
}

Run the code in your browser (opens in a new tab)