- Declare a struct, create it with a literal and print it with
%+v - Use pointers with
&and* - Choose correctly between value and pointer receivers
- Work with
NewXconstructor functions and struct embedding
Information about a student — name, age, average score — belongs together. Keeping it in three separate variables is awkward. Go has no classes; instead it has structs, which group related fields into one type, and methods, which add behaviour to that type. In this lesson you will also learn pointers — without them a function cannot change the original data.
Structs
A struct is declared as type Name struct { ... }, listing its fields and their types. To create a value you write a struct literal: Student{Name: "Aysel", Age: 15}. Fields are accessed with a dot: s.Name. Any field you don't set gets its zero value, so even an empty struct is a fully usable value.
package main
import "fmt"
type Student struct {
Name string
Age int
Score float64
}
func main() {
s := Student{Name: "Aysel", Age: 15, Score: 92.5}
var empty Student
s.Age++
fmt.Println(s)
fmt.Printf("%+v\n", s)
fmt.Printf("%+v\n", empty)
fmt.Println(s.Name, s.Score)
}{Aysel 16 92.5}
{Name:Aysel Age:16 Score:92.5}
{Name: Age:0 Score:0}
Aysel 92.5%v (and Println) show only the values, while %+v adds the field names — very handy when debugging. Structs whose fields are all comparable can be compared with ==: two Student values are equal when all their fields are equal.
Pointers
In Go everything passed to a function is copied. If the function changes its parameter, the caller's variable stays untouched. To change the original you need a pointer — the variable's address in memory. &x takes the address, and *p reads or changes the value at that address. The type *int means “pointer to an int”. Go has no pointer arithmetic (no p++ as in C), so pointers are safe.
package main
import "fmt"
func doubleValue(n int) {
n *= 2
}
func doublePointer(n *int) {
*n *= 2
}
func main() {
x := 5
p := &x
*p = 7
fmt.Println(x, *p, p == &x)
doubleValue(x)
fmt.Println(x)
doublePointer(&x)
fmt.Println(x)
}7 7 true 7 14
A value that holds the memory address of another variable. Its zero value is nil. & takes an address, and * accesses the value stored at it.
Methods: value and pointer receivers
A method is a function with a special parameter called the receiver. The receiver is written between the word func and the method name: func (a Account) Summary() string. If the receiver is a value (a Account), the method works on a copy of the object. If it is a pointer (a *Account), the method can change the original object.
package main
import "fmt"
type Account struct {
Owner string
Balance float64
}
func (a Account) Summary() string {
return fmt.Sprintf("%s: %.2f AZN", a.Owner, a.Balance)
}
func (a *Account) Deposit(amount float64) {
a.Balance += amount
}
func (a Account) BrokenDeposit(amount float64) {
a.Balance += amount
}
func main() {
acc := Account{Owner: "Murad", Balance: 100}
acc.Deposit(50)
acc.BrokenDeposit(1000)
fmt.Println(acc.Summary())
p := &acc
p.Deposit(25.5)
fmt.Println(p.Summary())
}Murad: 150.00 AZN Murad: 175.50 AZN
BrokenDeposit tried to add 1000 manat, but the balance did not change, because the method worked on a copy. Notice that acc is not a pointer, yet acc.Deposit(50) works — Go automatically calls (&acc).Deposit(50). In the same way you can call the value-receiver method Summary through the pointer p.
| Receiver | When to choose it |
|---|---|
func (a Account) | The method does not change the object and the type is small (a few fields, like time.Time). |
func (a *Account) | The method changes fields, the type is large, or it contains a field that must not be copied, such as sync.Mutex. |
type Counter struct {
value int
}
func (c Counter) Increment() {
c.value++ // changes a copy
}type Counter struct {
value int
}
func (c *Counter) Increment() {
c.value++ // changes the original
}Constructors and embedding
Go has no special constructors. Instead you write an ordinary function whose name starts with New: NewTeacher(...) creates an object and returns a pointer to it. There is no inheritance either — Go prefers composition. You can embed one struct in another as a field without a name: the embedded struct's fields and methods then become directly available on the outer struct.
package main
import "fmt"
type Person struct {
Name string
}
func (p Person) Greet() string {
return "Hi, I am " + p.Name
}
type Teacher struct {
Person
Subject string
}
func NewTeacher(name, subject string) *Teacher {
return &Teacher{Person: Person{Name: name}, Subject: subject}
}
func main() {
t := NewTeacher("Leyla", "Math")
fmt.Println(t.Greet())
fmt.Println(t.Name, "teaches", t.Subject)
fmt.Printf("%+v\n", *t)
}Hi, I am Leyla
Leyla teaches Math
{Person:{Name:Leyla} Subject:Math}Key points
- A struct groups related fields into one type;
%+vprints it with field names. - Go passes everything by value; to change the original you need a pointer (
&x,*p). - A value receiver works on a copy, a pointer receiver on the original; Go adds the
&for you. - Accessing a field through a
nilpointer causes a panic. - Go uses
NewXfunctions instead of constructors, and struct embedding (composition) instead of inheritance.
Check yourself
10 questions. Every correct answer earns XP.
fmt.Printf("%+v", Point{X: 1, Y: 2}) print?