Getting to know about structs
Structs represent a collection of variables. In the real world, we work with data all the time that would be well represented by a struct. For example, any form that is filled out in a job application or a vaccine card is a collection of variables (for example, last name, first name, and government ID number) that each has types (for example, string
, int
, and float64
) and are grouped together. That grouping would be a struct in Go.
Declaring a struct
There are two methods for declaring a struct. The first way is uncommon except in tests, as it doesn't allow us to reuse the struct's definition to create more variables. But, as we will see it later in tests, we will cover it here:
var record = struct{ Name string Age int }{ Name: "John Doak", Age: 100, // Yeah, not publishing the real one }
Here, we created a struct that contains two fields:
Name
(string
)Age
(int
)
We then created an instance of that struct that has those values set. To access those fields, we can use the dot .
operator:
fmt.Printf("%s is %d years old\n", record.Name, record.Age)
This prints "John Doak is 100 years old"
.
Declaring single-use structs, as we have here, is rarely done. Structs become more useful when they are used to create custom types in Go that are reusable. Let's have a look at how we can do that next.
Declaring a custom type
So far, we have created a single-use struct, which generally is not useful. Before we talk about the more common way to do this, let's talk about creating custom types.
Up until this point, we've seen the basic and pointer-wrapped types that are defined by the language: string
, bool
, map
, and slice
, for example. We can create our own types based on these basic types using the type
keyword. Let's create a new type called CarModel
that is based on the string
type:
type CarModel string
CarModel
is now its own type, just like string
. While CarModel
is based on a string
type, it is a distinct type. You cannot use CarModel
in place of a string or vice versa.
Creating a variable of CarModel
can be done similar to a string
type:
var myCar CarModel = "Chevelle"
Or, by using type conversion, as shown here:
myCar = CarModel("Chevelle")
Because CarModel
is based on string
, we can convert CarModel
back to string
with type conversion:
myCarAsString := string(myCar)
We can create new types based on any other type, including maps, slices, and functions. This can be useful for naming purposes or adding custom methods to a type (we will talk about this in a moment).
Custom struct types
The most common way to declare a struct is using the type
keyword. Let's create that record again, but this time let's make it reusable by declaring a type:
type Record struct{ Name string Age int } func main() { david := Record{Name: "David Justice", Age: 28} sarah := Record{Name: "Sarah Murphy", Age: 28} fmt.Printf("%+v\n", david) fmt.Printf("%+v\n", sarah) }
By using type
, we have made a new type called Record
that we can use again and again to create variables holding Name
and Age
.
Note
Similar to how you may define two variables with the same type on a single line, you may do the same within a struct
type, such as First, Last string
.
Adding methods to a type
A method is similar to a function, but instead of being independent, it is bound to a type. For example, we have been using the fmt.Println()
function. That function is independent of any variable that has been declared.
A method is a function that is attached to a variable. It can only be used on a variable of a type. Let's create a method that returns a string representation of the Record
type we created earlier:
type Record struct{ Name string Age int } // String returns a csv representing our record. func (r Record) String() string { return fmt.Sprintf("%s,%d", r.Name, r.Age) }
Notice func (r Record)
, which attaches the function as a method onto the Record
struct. You can access the fields of Record
within this method by using r.<field>
, such as r.Name
or r.Age
.
This method cannot be used outside of a Record
object. Here's an example of using it:
john := Record{Name: "John Doak", Age: 100} fmt.Println(john.String())
Let's look at how we change a field's value.
Changing a field's value
Struct values can be changed by using the variable attribute followed by =
and the new value. Here is an example:
myRecord.Name = "Peter Griffin" fmt.Println(myRecord.Name) // Prints: Peter Griffin
It is important to remember that a struct is not a reference type. If you pass a variable representing a struct to a function and change a field in the function, it will not change on the outside. Here is an example:
func changeName(r Record) { r.Name = "Peter" fmt.Println("inside changeName: ", r.Name) } func main() { rec := Record{Name: "John"} changeName(rec) fmt.Println("main: ", rec.Name) }
This will output the following:
Inside changeName: Peter Main: John
As we learned in the section on pointers, this is because the variable is copied, and we are changing the copy. For struct types that need to have fields that change, we normally pass in a pointer. Let's try this again, using pointers:
func changeName(r *Record) { r.Name = "Peter" fmt.Println("inside changeName: ", r.Name) } func main() { // Create a pointer to a Record rec := &Record{Name: "John"} changeName(rec) fmt.Println("main: ", rec.Name) } Inside changeName: Peter Main: Peter
This will output the following:
Inside changeName: Peter Main: Peter
Note that .
is a magic operator that works on struct
or *struct
.
When I declared the rec
variable, I did not set the age
. Non-set fields are set to the zero value of the type. In the case of Age
, which is int
, this would be 0
.
Changing a field's value in a method
In the same way that a function cannot alter a non-pointer struct, neither can a method. If we had a method called IncrAge()
that increased the age on the record by one, this would not do what you wanted:
func (r Record) IncrAge() { r.Age++ }
The preceding code passes a copy of Record
, adds one to the copy's Age
, and returns.
To actually increment the age, simple make Record
a pointer, as follows:
func (r *Record) IncrAge() { r.Age++ }
This will work as expected.
Tip
Here is a basic rule that will keep you out of trouble, especially when you are new to the language. If the struct
type should be a pointer, then make all methods pointer methods. If it shouldn't be, then make them all non-pointers. Don't mix and match.
Constructors
In many languages, constructors are specially-declared methods or syntax that are used to initialize fields in an object and sometimes run internal methods as setup. Go doesn't provide any specialized code for that, instead, we use a constructor pattern using simple functions.
Constructors are commonly either called New()
or New[Type]()
when declaring a public constructor. Use New()
if there are no other types in the package (and most likely won't be in the future).
If we wanted to create a constructor that made our Record
from the previous section, it might look like the following:
func NewRecord(name string, age int) (*Record, error) { if name == "" { return nil, fmt.Errorf("name cannot be the empty string") } if age <= 0 { return nil, fmt.Errorf("age cannot be <= 0") } return &Record{Name: name, Age: age}, nil }
This constructor takes in a name
and age
argument and returns a pointer to Record
with those fields set. If we pass bad values for those fields, it instead returns the pointer's zero value (nil
) and an error. Using this looks like the following:
rec, err := NewRecord("John Doak", 100) if err != nil { return err }
Don't worry about the error, as we will discuss it in the course of the book's journey.
By now, you have learned how to use struct
, Go's base object type. This included creating a struct, creating custom structs, adding methods, changing field values, and creating constructor functions. Now, let's look at using Go interfaces to abstract types.