Hey there! Let's learn structs in Golang.
Struct
Struct (aka structure) is used to create custom data types. In Golang we have primitive data types like int, float, complex, boolean, etc, and composite data types, such as arrays, maps, etc. But, what if we want a combination of int and float data types together as a single data type? You guessed it; we use struct.
We use Struct to create a combination of different data types(essentially a combination of fields). Its syntax is as follows
type structName struct {
field1 data type
field2 data type
....
}
In the line type structName struct
, the words type
and struct
are keywords, and structName
is the name that we'd give to our custom struct. Followed by fields and their data types.
package main
type customStruct struct {
num int
balance float32
}
The above struct customStruct
is a custom data type holding int
and float32
data types. So far so good, but how do we use that struct?
Initialization of struct
So far we have a custom struct defined; we know only after we initialize we can access it because that's when memory allocation is done. To initialize a struct we can do the following ways:
Struct literal
new() function
general
Struct literal
If we provide values right away when we assign struct to some variable
package main
import "fmt"
type customStruct struct {
num int
balance float32
}
func main() {
obj := customStruct{12, 1300.25}
fmt.Println(obj) // prints {12 1300.25}
}
The variable
obj
is known as an object/instance of the structcustomStruct
We can even use field names while specifying arguments in the declaration as shown below, which helps when we don't want to follow the order when providing values. This is known as
Name
syntax.
package main
import "fmt"
type customStruct struct {
num int
balance float32
}
func main() {
// order doesn't matter
obj := customStruct{balance:1300.25, num:12}
fmt.Println(obj) // prints {12 13.25}
}
New() function
We could also use new() build-in function to initialize a stuct.
package main
import "fmt"
type employee struct {
name string
age int
salary float32
}
func main() {
obj := new(employee)
fmt.Println(obj) // prints &{ 0 0}
}
As you can see new() allocates memory and returns us the reference to that object(we'll cover reference vs value).
Of course, when a data type is initialized, default values will be created. That's why &{ 0 0} in output (default values for string is ""
, and 0
for int and float)
To assign values to fields, we use dot notation as shown below.
package main
import "fmt"
type employee struct {
name string
age int
salary float32
}
func main() {
obj := new(employee)
// objectName.fieldName = value
obj.name = "Satvik"
obj.age = 25
obj.salary = 75000.00
fmt.Println(obj) // prints &{Satvik 25 75000}
}
General declaration
package main
import "fmt"
type employee struct {
name string
age int
salary float32
}
func main() {
var obj employee
obj.name = "Satvik"
obj.age = 25
obj.salary = 75000.00
fmt.Println(obj) // prints {Satvik 25 75000}
}
Accessing struct fields
Just like we assigned values to object fields, we use the same syntax to access them.
package main
import "fmt"
type employee struct {
name string
age int
salary float32
}
func main() {
obj := employee{
name: "Satvik",
age: 25,
salary: 75000.00,
}
fmt.Println(obj.name) // prints Satvik
fmt.Println(obj.age) // prints 25
fmt.Println(obj.salary) // prints 75000.50
}
- if you've observed, I've provided field/key names(ex:
name: "Satvik"
) while declaring the object, it's best practice to use key names while using literal declaration for creating an object of a struct.
Update struct fields
Updating struct fields is just as same as assigning values to stuct fields
package main
import "fmt"
type employee struct {
name string
age int
salary float32
}
func main() {
obj := employee{
name: "Satvik",
age: 25,
salary: 75000.00,
}
obj.name = "Mahi"
fmt.Println(obj.name) // prints Mahi
fmt.Println(obj.age) // prints 25
fmt.Println(obj.salary) // prints 75000
}
Pointer vs Value struct instances
Let's first see what a pointer struct looks like...
package main
import "fmt"
type employee struct {
name string
age int
salary float32
}
func main() {
obj := &employee{
name: "Satvik",
age: 25,
salary: 75000.00,
}
fmt.Println(obj) // prints &{Satvik 25 75000}
}
As shown above, before struct literal I've used ampersand (&)
symbol, that makes the struct object obj
a pointer, meaning the variable obj
now stores the address of the struct rather than the struct value.
If you can recall, even when we'd used new() to initialize struct the output did have &
, that says, even new() is returning us a pointer.
Wonder why &
is being printed in the output? It's because Go is dereferencing the obj pointer for us, to get the struct values being referenced by that address. (If not, we'd need to do that by ourselves by *obj
)
Observe the following.
package main
import (
"fmt"
"reflect"
)
type employee struct {
name string
age int
salary float32
}
func main() {
obj1 := employee{
name: "Satvik",
age: 25,
salary: 75000.00,
}
obj2 := &employee{
name: "Satvik",
age: 25,
salary: 75000.00,
}
fmt.Println(reflect.ValueOf(obj1).Kind()) // prints struct
fmt.Println(reflect.ValueOf(obj2).Kind()) // prints ptr
}
I've declared two objects for the employee struct with different syntaxes, obj1
and obj2
where the latter use pointer-based. If you have observed the output of the two prints statements, obj1
kind is struct
whereas obj2
kind is ptr
, which states that obj2
is holding an address. (we use reflect package to determine the data types and kind of variables)
Why bother with pointer vs value struct
In Golang everything is pass by value. We can pass by value, a copy of an object, or pass by value, an address of an object.
The point here is, when to use value struct and when to go with pointer struct; it depends. Does it matter if we just use value struct rather than pointer struct? Yes. Why? It's because of Go's pass by value concept, using direct value struct may cause unintended behaviour in code.
Check out this in-detail blog https://www.ardanlabs.com/blog/2013/09/iterating-over-slices-in-go.html by William Kennedy to understand the caveats of using value struct.
Copying structs
We can copy struct of one instance both by value and pointer reference
package main
import (
"fmt"
"reflect"
)
type employee struct {
name string
age int
salary float32
}
func main() {
obj := employee{
name: "Satvik",
age: 25,
salary: 75000.00,
}
objCopy1 := obj // coping uisng value of obj
objCopy1.name = "Steve"
fmt.Println(obj.name) // Satvik
fmt.Println(objCopy1.name) // Steve
objCopy2 := &obj // copy using reference of obj
objCopy2.name = "Emma"
fmt.Println(obj.name) // Emma
fmt.Println(objCopy2.name) // Emma
}
As you can see the difference, one that copied by value objCopy1 := obj
didn't have any effect on the original instance when modified the copied instance objCopy1
, but the one copied with using the address of obj
affected original instance when altered copied instance objCopy2
.
Nested Structs
We can nest a struct inside another struct to create more complex yet more readable code.
package main
import (
"fmt"
"reflect"
)
type address struct {
location string
country string
}
type employee struct {
name string
age int
salary float32
address address
}
func main() {
obj1 := employee{
name: "Satvik",
age: 25,
salary: 75000.00,
address: address{location: "Goa", country: "India"},
}
fmt.Println(obj1.address.location) // prints Goa
}
In the above code, we have nested address
struct in employee
struct thus obj1
holds all the data needed of an employee.
Just like we create a struct literal, we even assign values to nested struct the same way. And accessing and updating values is with dot notation.
Methods to struct
A method is basically a function but bound to a specific struct. If you know OOP paradigm, this is just as same as method of class, where class here is a struct. I mean Go doesn't support OOP paradigm, but this struct and methods of struct kinda resemble the same.
Let's create a printInfo method to employee struct
package main
import (
"fmt"
"reflect"
)
type employee struct {
name string
age int
salary float32
}
func (e employee) printInfo() {
fmt.Println("Name is", e.name)
fmt.Println("Age is", e.age)
fmt.Println("Salary is", e.salary)
}
func main() {
obj := employee{
name: "Satvik",
age: 25,
salary: 75000.00,
}
obj.printInfo()
}
// output
// Name is Satvik
// Age is 25
// Salary is 75000
If you've observed the method printInfo()
syntax is almost same as a general function, but with (e employee)
prefixed to printInfo()
. That prefix is what differentiates the general function from a method, and also indicates that this method is bound to type employee
struct.
In func (e employee) printInfo() { }
, the e
is random, we can choose any variable as long the as the type employee
is mentioned correctly. That's basically just like an argument we pass to a function.
When we call the method printInfo()
using the synatx obj.printInfo()
, the obj is passed as pass by value to that method, and that's how we can access the name, age, salary fields using the dot notation by calling e.name, e.age, etc.
Pointer struct to methods
If we want to make modifications to struct fields, we'd pass the struct as pass by address, rather than pass by value.
package main
import (
"fmt"
"reflect"
)
type employee struct {
name string
age int
salary float32
}
func (e *employee) changeName() {
e.name = "Changed"
}
func main() {
obj := employee{
name: "Satvik",
age: 25,
salary: 75000.00,
}
obj.changeName()
fmt.Println("Name", obj.name) // Name Changed
}
Observe the syntax func (e *employee) changeName(){ }
, this time the type of e is a pointer struct. So, the obj
is passed as passed by address to changeName()
method, hence the modifications persist after the method call.
That's it for structs. Hope you learned something new. Peace out!