Go supports object-oriented programming concepts through structs and methods. A struct can have fields (attributes) and methods, and interfaces define shared behavior.
Struct creates a user-defined data type, which holds its own fields (attributes) and methods, which can be accessed and used by creating a value of that struct type.
Structs are like a blueprint for values outlining possible behaviors and states that every value of a certain type could have.
Structs are defined via the keywords type and struct, like this:
// Define an empty struct Employee
type Employee struct {
}
Values/Instances
A value is an instance of a struct type. A struct is like a blueprint while a value is a copy of the struct with actual data. It's not an idea anymore, it's an actual employee, like a person named "Andrew" who is 30 years old.
You can have many employees to create many different values, but without the struct as a guide, you would be lost, not knowing what information is required.
After a struct is defined, we can create values of that type. There are two ways of creating values: directly and via pointer.
Direct values
Direct values behave the same as local variables. They only exist within the scope where they were created. We create a direct value just like declaring a variable:
// Define an empty struct Employee
type Employee struct {
}
func mainFunction() {
var employee Employee
}
Pointer values
Pointer values are not bounded by the existence of the scope in which they were created. For example, pointer values created inside a function continue to exist even after the function returns.
A pointer value is created using the & operator that returns a pointer to the newly constructed value:
// Define an empty struct Employee
type Employee struct {
}
func mainFunction() {
employee := &Employee{}
}
Notice the &Employee{}. This creates a pointer to an Employee struct. We use this pointer to access and manipulate the actual value, just like we use a remote controller for a TV.
Fields/Properties
A struct field is a variable that belongs to a struct. Let's add some fields to our Employee struct:
type Employee struct {
name string
age int
}
func mainFunction() {
emp1 := Employee{} // direct value
emp1.name = "Andrew"
fmt.Println(emp1.name) // prints "Andrew"
emp2 := &Employee{} // pointer to struct value
emp2.age = 40
fmt.Println(emp2.age) // prints 40
}
We added two fields: the string name and the int age. Then, we created two different Employee values emp1 and emp2.
The fields of direct values can be accessed and modified using the dot operator (.), like we did above with emp1.name.
The fields of pointer values are also accessed using the dot operator (.), like we did above with emp2.age — Go automatically dereferences the pointer.
In Go, fields starting with an uppercase letter are exported (accessible from other packages), while lowercase fields are only accessible within the same package.
Constructors
In real life, we want to set the fields specific to each value when creating it. For example, an employee named Andrew of 30 years old and another one named Mary of 25 years old. Here the constructor function comes to rescue.
A constructor function is a regular function used to initialize and return a new struct value. By convention it is named New followed by the struct name.
In Go there is no built-in constructor, so we define our own constructor function:
type Employee struct {
name string
age int
}
// constructor function:
func NewEmployee(name string, age int) Employee {
return Employee{name: name, age: age}
}
func mainFunction() {
emp1 := NewEmployee("Andrew", 30) // emp1.name = "Andrew", emp1.age = 30
fmt.Println(emp1.name)
emp2 := &Employee{name: "Mary", age: 25} // emp2.name = "Mary", emp2.age = 25
fmt.Println(emp2.age)
}
The receiver
In Go, methods are functions with a special first parameter called a receiver. The receiver gives the method access to the struct's fields.
The receiver is explicitly declared in the method signature. Inside a method, use the receiver name to access and manipulate the fields of that value, like we do with e.name.
Methods
Methods are functions that belong to the struct type:
type Employee struct {
name string
age int
}
func NewEmployee(name string, age int) Employee {
return Employee{name: name, age: age}
}
// methods:
func (e Employee) printName() {
fmt.Println(e.name)
}
func (e Employee) printEmployee() {
e.printName()
fmt.Println(e.age)
}
func mainFunction() {
emp1 := NewEmployee("Andrew", 30)
emp1.printEmployee() // prints "Andrew", 30
emp2 := &Employee{name: "Mary", age: 25}
emp2.printEmployee() // prints "Mary", 25
}
As you can see, we access methods just like we access fields: using the dot syntax (.) for both direct values and pointer values — Go automatically dereferences pointers when calling methods.
The method call uses the fields of the value it was called on. That's why the first printEmployee() prints "Andrew" and 30 and the second prints "Mary" and 25.
Value vs Pointer receivers
In Go, a method receiver can be either a value receiver or a pointer receiver. A value receiver receives a copy of the struct and cannot modify the original. A pointer receiver receives a pointer to the struct and can modify the original.
type Employee struct {
name string
age int
}
func NewEmployee(name string, age int) Employee {
return Employee{name: name, age: age}
}
// value receiver - reads but does not modify the original:
func (e Employee) printName() {
fmt.Println(e.name)
}
// pointer receiver - can modify the original:
func (e *Employee) birthday() {
e.age++
}
func mainFunction() {
emp1 := NewEmployee("Andrew", 30)
emp1.printName() // prints "Andrew"
emp2 := &Employee{name: "Mary", age: 25}
emp2.birthday()
fmt.Println(emp2.age) // prints 26
}
Assignment
Follow the Coding Tutorial and let's write some structs.
Hint
Look at the examples above if you get stuck.
Free plan: 30 tutor messages. PRO is unlimited.
