Structs in Go
Struct (short for “structures”) are a powerful technique to combine related data into a single unit in Go, which enables you to define custom types. They are essential for structuring data, establishing the environment in which a Go program runs, providing a natural location for documentation, and guaranteeing that data is used correctly.
Go is not an object-oriented (OO) language in the conventional sense, but it makes use of structs and methods to accomplish the same functionality as classes in other languages, which makes working with complicated data structures neat and orderly.
Defining a Struct
The type keyword, the struct name, the struct keyword, and a list of fields encapsulated in curly brackets {} are the steps used to define a struct. Every field has a name and a type of data. If several fields are of the same type, you can define them all on one line with commas between them.
Example of Struct Definition:
package main
import "fmt"
// Define a struct named 'Circle' with three float64 fields
type Circle struct {
x float64
y float64
r float64
}
// Alternatively, collapse fields of the same type
type Rectangle struct {
x1, y1, x2, y2 float64
}
func main() {
// Creating an instance of Circle
myCircle := Circle{x: 10.0, y: 5.0, r: 3.0}
fmt.Println("--- Circle Details ---")
fmt.Printf("Circle: X=%.1f, Y=%.1f, Radius=%.1f\n", myCircle.x, myCircle.y, myCircle.r)
// You can also initialize without field names, if the order is correct
anotherCircle := Circle{1.0, 2.0, 4.0}
fmt.Printf("Another Circle: X=%.1f, Y=%.1f, Radius=%.1f\n", anotherCircle.x, anotherCircle.y, anotherCircle.r)
fmt.Println("\n--- Rectangle Details ---")
// Creating an instance of Rectangle
myRectangle := Rectangle{x1: 0.0, y1: 0.0, x2: 10.0, y2: 5.0}
fmt.Printf("Rectangle: X1=%.1f, Y1=%.1f, X2=%.1f, Y2=%.1f\n", myRectangle.x1, myRectangle.y1, myRectangle.x2, myRectangle.y2)
// Accessing and modifying fields
myCircle.x = 15.0 // Change the X coordinate of myCircle
fmt.Printf("Updated Circle X: %.1f\n", myCircle.x)
myRectangle.y1 = 2.0 // Change Y1 coordinate of myRectangle
fmt.Printf("Updated Rectangle Y1: %.1f\n", myRectangle.y1)
}
Output
--- Circle Details ---
Circle: X=10.0, Y=5.0, Radius=3.0
Another Circle: X=1.0, Y=2.0, Radius=4.0
--- Rectangle Details ---
Rectangle: X1=0.0, Y1=0.0, X2=10.0, Y2=5.0
Updated Circle X: 15.0
Updated Rectangle Y1: 2.0
Initializing Structs
A struct instance can be made in a number of ways:
Zero Value Initialization: A struct variable will be initialized to its zero values if it is declared without any values assigned to it (e.g., 0 for integers/floats, “” for strings, nil for pointers, etc.).
Using new
Function: A pointer to the freshly constructed struct is returned by the built-in new function, which also creates memory for each field and sets it to zero.
Composite Literals: You can declare and initialize a struct in a single step using this method, which is the most popular way to do so.
- With Field Names: You can enter values for particular fields in any order using this form. The value zero will be assigned to fields that are not listed. Since it can withstand changes in field order or the addition of new fields, this is usually chosen.
- Without Field Names: If you offer a value in the precise order that each field is defined in the struct, you can skip the field names. The code may break if fields are added or rearranged, hence this form works best for stable types with minimal fields.
- Getting a Pointer with Composite Literals: To directly create a pointer to a struct, prefix a composite literal with the & (address) operator.
Accessing Struct Fields
You use the. (dot) operator to access a struct’s fields. When you have a pointer to a struct, Go offers a “ergonomic amenity” that allows you to use the dot operator without explicitly dereferenceing the pointer (*).
Example:
package main
import "fmt"
type Circle struct {
x, y, r int
}
func main() {
c := Circle{0, 0, 5}
fmt.Println(c.x, c.y, c.r)
c.x = 10
fmt.Println(c.x)
cPtr := &Circle{0, 0, 5}
fmt.Println(cPtr.x)
(*cPtr).y = 20
fmt.Println(cPtr.y)
}
Output
0 0 5
10
0
20
Pointers and Structs
When combined with structs, pointers are quite helpful, particularly when you need a function or method to change the original struct instance. The method obtains a copy of the struct when it is supplied to it by value; modifications made inside the function have no effect on the original. A pointer to the struct must be given in order to enable mutation.
Example (Passing by Value vs. Passing by Pointer):
package main
import "fmt"
type Creature struct {
Species string
}
// This function receives a COPY of the Creature value
func changeCreatureByValue(c Creature) {
c.Species = "jellyfish"
fmt.Printf("Inside changeCreatureByValue: %+v\n", c)
}
// This function receives a POINTER to the Creature
func changeCreatureByPointer(cPtr *Creature) {
if cPtr == nil { // Important to check for nil pointers before dereferencing
fmt.Println("Creature pointer is nil")
return
}
cPtr.Species = "jellyfish" // Go automatically dereferences cPtr here
fmt.Printf("Inside changeCreatureByPointer: %+v\n", cPtr)
}
func main() {
myCreature := Creature{Species: "shark"}
fmt.Printf("Before changeByValue: %+v\n", myCreature)
changeCreatureByValue(myCreature)
fmt.Printf("After changeByValue: %+v\n", myCreature)
fmt.Println("---")
anotherCreature := Creature{Species: "shark"}
fmt.Printf("Before changeByPointer: %+v\n", anotherCreature)
changeCreatureByPointer(&anotherCreature) // Pass the address of anotherCreature
fmt.Printf("After changeByPointer: %+v\n", anotherCreature)
}
Output
Before changeByValue: {Species:shark}
Inside changeCreatureByValue: {Species:jellyfish}
After changeByValue: {Species:shark}
---
Before changeByPointer: {Species:shark}
Inside changeCreatureByPointer: &{Species:jellyfish}
After changeByPointer: {Species:jellyfish}
You can retrieve the memory address of a particular field inside a struct using Go’s support for interior pointers without having to declare the field as a pointer.
Methods on Structs
Methods are functions associated with a certain type that have a unique receiver argument. A method that is specified on a struct gives the structured data behaviour.
Value Receivers: A method that works on a copy of the struct instance is said to have a value receiver (e.g., (c Creature)). The initial struct instance will not be impacted if the receiver is changed within the method.
Pointer Receivers: A pointer to the struct instance is sent to a method that has a pointer recipient (for example, (c *Creature)). As a result, the procedure can alter the fields of the original instance. while using dot notation to call a method with a pointer receiver (myCreature, for example).Go gets the variable’s address automatically when you use Reset(), saving you from having to write (&myCreature).reset().
Example (Value vs. Pointer Receivers):
package main
import "fmt"
type Player struct {
Name string
Score int
}
// Method with a VALUE receiver: operates on a copy of Player
func (p Player) IncreaseScoreValue() {
p.Score++ // This changes the COPY, not the original
fmt.Printf("Inside IncreaseScoreValue (copy): %+v\n", p)
}
// Method with a POINTER receiver: operates on the original Player instance
func (p *Player) IncreaseScorePointer() {
p.Score++ // This changes the ORIGINAL
fmt.Printf("Inside IncreaseScorePointer (original): %+v\n", p)
}
func main() {
player1 := Player{Name: "Alice", Score: 10}
fmt.Printf("Before IncreaseScoreValue: %+v\n", player1)
player1.IncreaseScoreValue()
fmt.Printf("After IncreaseScoreValue: %+v\n", player1)
fmt.Println("---")
player2 := Player{Name: "Bob", Score: 10}
fmt.Printf("Before IncreaseScorePointer: %+v\n", player2)
player2.IncreaseScorePointer() // Go automatically takes &player2
fmt.Printf("After IncreaseScorePointer: %+v\n", player2)
}
Output
Before IncreaseScoreValue: {Name:Alice Score:10}
Inside IncreaseScoreValue (copy): {Name:Alice Score:11}
After IncreaseScoreValue: {Name:Alice Score:10}
---
Before IncreaseScorePointer: {Name:Bob Score:10}
Inside IncreaseScorePointer (original): &{Name:Bob Score:11}
After IncreaseScorePointer: {Name:Bob Score:11}
Structs in Collections and with Tags
Arrays and slices are examples of collections that can use structures as items. This makes it possible to combine values that are related together, which improves the organization of data administration.
Small bits of metadata called “struct tags” can be appended to a struct’s fields. They give other Go code that uses the struct instructions, particularly for tasks like encoding and decoding JSON.
Example (JSON struct tags):
package main
import (
"encoding/json"
"fmt"
"log"
)
type User struct {
Name string `json:"full_name"` // Field will be named 'full_name' in JSON
Age int `json:"age,omitempty"` // Field will be 'age', omitted if zero value
IsActive bool `json:"-"` // Field will be ignored in JSON
privateField string // Unexported fields are ignored by default
}
func main() {
user := User{
Name: "John Doe",
Age: 30,
IsActive: true,
privateField: "secret",
}
jsonData, err := json.MarshalIndent(user, "", " ") // Use MarshalIndent for pretty printing
if err != nil {
log.Fatal(err)
}
fmt.Println(string(jsonData))
}
Output
{
"full_name": "John Doe",
"age": 30
}
Embedded Types (Composition)
Embedded types (sometimes termed anonymous fields or composition) are a technique that Go enables that allows a struct to directly embed another type without a field name. By eschewing conventional inheritance, Go models “is-a” relationships (for example, “an Android is a Person”) and forwarding techniques. Embedded type methods are immediately available from the outer struct.
Example:
package main
import "fmt"
type Person struct {
Name string
}
func (p *Person) Talk() {
fmt.Println("Hi, my name is", p.Name)
}
type Android struct {
Person // Embedded type (anonymous field)
Model string
}
func main() {
a := Android{}
a.Person.Name = "Robo" // Access embedded field explicitly
a.Model = "A100"
a.Person.Talk() // Call method on embedded type explicitly
// Go's "ergonomic amenity" allows direct access to embedded fields and methods
a.Name = "Unit 734" // Same as a.Person.Name = "Unit 734"
a.Talk() // Same as a.Person.Talk()
fmt.Println(a.Model)
}
Output
Hi, my name is Robo
Hi, my name is Unit 734
A100
You can also read Functions In Go Structure, Declaration With Code Examples