mdawar.dev

A blog about programming, Web development, Open Source, Linux and DevOps.

Go - Structs

Struct Definition

  • A struct is a composite data type that groups named values together as a single entity
  • Each value in a struct is called a field
  • A field is exported if it begins with a capital letter
  • The struct fields can be of any data type
go
// A struct is a sequence of named elements called fields.
type Person struct {
  // Each field has a name and a type.
  Name string

  // The field is exported if it begins with a capital letter.
  LastName string

  // Unexported field (accessible only in the declaring package).
  salary int

  // Fields of the same type may be declared separated by commas.
  // Usually related fields of the same type are combined.
  Weight, Height float64

  IsEmployed bool
}

Struct Zero Value

The zero value for a struct is composed of the zero values of each of its fields.

go
var person Person

person.Name       // ""
person.LastName   // ""
person.salary     // 0 (Accessible only in the declaring package)
person.Weight     // 0
person.Height     // 0
person.IsEmployed // false

Struct Instance

go
type Point struct {
  X, Y int
}

// Create an instance of the `Point` struct.
// The struct fields will be initialized to their zero values.
var p1 Point // Point{X:0, Y:0}

// Create an instance using the struct literal syntax
// with all the fields initialized to their zero values.
p2 := Point{} // Point{X:0, Y:0}

// Create an instance of a struct with initial field values
// Set the fields by listing their names and values.
p3 := Point{X: 1, Y: 2} // Point{X:1, Y:2}

// The order of the fields does not matter when specifying their names.
p4 := Point{Y: 2, X: 1} // Point{X:1, Y:2}

// Omitted fields are set to their zero values.
p5 := Point{Y: 3} // Point{X:0, Y:3}

// Set the field values using their order.
// In this form the values must be set for every field in the right order.
// This form is used with small struct types like this one.
p6 := Point{4, 5} // Point{X:4, Y:5}

Pointer to Struct Instance

go
type Point struct {
  X, Y int
}

// Using the address operator with a struct instance.
var point Point
p1 := &point // *Point

// Using the address operator with a struct literal syntax.
p2 := &Point{} // *Point

// Using the `new` function.
p3 := new(Point) // *Point

Struct Fields

Fields are accessed using dot notations.

go
type Point struct {
  X, Y int
}

var point Point

// Get the value of a field.
x := point.X

// Set the value of a field.
point.X = 5

// A pointer to the struct.
var p *Point = &point

// Set a field value using the struct pointer.
// Struct fields can be accessed through a struct pointer.
// Explicit dereferencing is not required.
p.X = 7

// Equivalent to
(*p).X = 7

// A pointer to a struct field.
y := &point.Y // *int

// Set the field value using a pointer.
*y = 10

Struct Methods

Methods can be defined on structs using receiver functions.

go
type Person struct {
  Name string
}

// Define a method on the `Person` struct.
func (p Person) Greet() {
  fmt.Printf("Hello, my name is %s", p.Name)
}

person := Person{"John"}

// Call a method on a struct instance.
person.Greet()

Struct Embedding

  • Struct embedding is used for code reuse and as a form of composition
  • A field declared with a type without an explicit field name is called an embedded field or an anonymous field
go
type inner struct {
  C int
}

type Central struct {
  B int

  // `inner` is embedded within Central.
  // The field name is `inner` (Unexported, starts with a lowercase letter).
  // Accessible in the declaring package only.
  inner
}

// A method defined on the `Central` type.
func (e Central) Method() error {
  return nil
}

type Outer struct {
  A int

  // `Central` is embedded within `Outer`.
  // The field name is `Central` (exported, starts with an uppercase letter).
  Central
}

// The struct literal syntax must follow the shape of the type delcaration.
outer := Outer{
  A: 1,

  Central: Central{
    B: 2,

    inner: inner{
      C: 3,
    },
  },
}

// Struct literal syntax using the fields order.
outer = Outer{1, Central{2, inner{3}}}

// Non embedded field.
outer.A

// The fields and methods of the embedded types will be available on the outer struct.
// The anonymous fields can be omitted when referring to their subfields and methods,
// they are called promoted fields and methods.
outer.B        // outer.Central.B
outer.C        // outer.Central.inner.C
outer.Method() // outer.Central.Method()

// The embedded fields and methods can be accessed explicitly.
outer.Central.B
outer.Central.Method()
outer.Central.inner.C // Accessible only in the declaring package
outer.inner.C         // Accessible only in the declaring package

Embedded Field Name

An anonymous field can be defined with any named type or a pointer to a named type and not only struct types.

The name and visibility of the anonymous field are implicitly determined by the type name.

go
type Example struct {
  // Anonymous field defined with a named type.
  T1 // field name: T1 (Exported field, starts with an uppercase letter)

  // Anonymous field defined with a pointer to a named type.
  *T2 // field name: T2

  // The unqualified type name acts as the field name.
  P.T3  // field name: T3
  *P.T4 // field name: T4

  // The first letter casing determines the field's visibility.
  t5 // field name: t5 (Unexported field, starts with a lowercase letter)

  *T1   // illegal: field names must be unique (Conflicts with T1)
  P.T2  // illegal: field names must be unique (Conflicts with T2)
}

Embedded Field Name Conflicts

If the embedded type has fields or methods with conflicting names, the outer struct’s fields and methods take precedence when using the shorthand notation.

go
type inner struct {
  A, B, C int
}

type Central struct {
  A, B int

  inner
}

type Outer struct {
  A int

  Central
}

outer := Outer{1, Central{2, 3, inner{4, 5, 6}}}

outer.A         // 1 (Outer.A takes precedence)
outer.Central.A // 2 (Central.A takes precedence)
outer.B         // 3 (Central.B takes precedence)
outer.inner.A   // 4 (Or: outer.Central.inner.A)
outer.inner.B   // 5 (Or: outer.Central.inner.B)
outer.C         // 6 (No conflicts)

Struct Arguments and Return Values

go
type Point struct { X, Y int }

// Struct values can be passed to and returned from functions.
func Scale(p Point, factor int) Point {
  return Point{p.X * factor, p.Y * factor}
}

// In Go, functions receive a copy of the argument.
// If a function needs to modify a struct value then a pointer must be passed.
func Reset(p *Point) {
  // The fields can be accessed without explicit pointer dereferencing.
  p.X = 0
  p.Y = 0
}

// Even if the function does not need to modify the struct
// a pointer may be used with large structs for efficiency
// to avoid copying the struct value.
func Calculate(s *LargeData) int {
  // ...
}

// The same can be used to return a large struct from a function.
func CreateLargeData() *LargeData {
  return &LargeData{
    // ...
  }
}

Comparing Structs

  • A struct is comparable if all of its fields are comparable
  • The == and != operations compare the fields of the structs in order
  • Comparable struct types may be used as the key type of a map
go
type Point struct { X, Y int }

var p1 Point  // Point{X:0, Y:0}

p2 := Point{} // Point{X:0, Y:0}

// Comparing structs compares their fields in order.
fmt.Println(p1 == p2) // true

// Same as comparing the struct fields one by one.
fmt.Println(p1.X == p2.X && p1.Y == p2.Y) // true

Field Tags

A field declaration may be followed by an optional string literal called a tag which becomes an attribute for the field or multiple fields if they are combined in one declaration.

go
type FieldTags struct {
  // A field declaration may be followed by an optinal tag (string literal).
  // The tag becomes an attribute for the field.
  example string "Any string can be used as a tag"

  // The tag becomes an attribute for all the fields in the same declaration.
  a, b int "Tag for both a and b"

  // An empty tag is like an absent tag.
  x float64 ""

  // By convention, tag strings are a concatenation of optionally space-separated
  // key:"value" pairs in a raw string literal (between back quotes).
  F string `key:"value" color:"blue"`
}

Tags can be accessed using the reflect standard library package otherwise they are ignored.

go
package main

import (
	"fmt"
	"reflect"
)

func main() {
	type Tags struct {
		TaggedField string `key:"value" color:"blue"`
	}

	s := Tags{}

	st := reflect.TypeOf(s)

	// Get the first field in the struct (panics on errors).
	field := st.Field(0)

	fmt.Println(field.Tag.Get("key"))   // "value"
	fmt.Println(field.Tag.Get("color")) // "color"

	// Or get the field by name.
	if field, ok := st.FieldByName("TaggedField"); !ok {
		fmt.Println("Field not found")
	} else {
		fmt.Println(field.Tag.Get("key"))   // "value"
		fmt.Println(field.Tag.Get("color")) // "color"
	}
}

Empty Struct

go
// Empty struct with no fields (has size zero).
struct{}

// When creating a map representing a set, the empty struct may be used
// instead of bool as the value type to save space.
// Note: the space saving is not significant.
set := make(map[string]struct{}) // Instead of map[string]bool

if _, ok := set["value"]; !ok {
  set["value"] = struct{}{}
}

Recursive Data Structures

A named struct type cannot declare a field of the same type.

To create recursive data structures, a struct can declare a field of a pointer type.

go
type tree struct {
  value       int
  left, right *tree
}

Field Order

The order of the struct fields can affect memory usage.

To save memory and improve performace, the struct fields can be ordered by their type size in descending order which can make the struct more efficient.

go
type Efficient struct {
  A int64 // 8 bytes
  B int32 // 4 bytes
  C int16 // 2 bytes
  D int8  // 1 byte
  E bool  // 1 byte
}

The fieldalignment analyzer part of staticcheck can be used to detect structs that would use less memory if their fields were sorted.

Read more: Data structure alignment