Go isn’t an object-oriented (OO) language, so it doesn’t have objects nor inheritance and thus, doesn’t have the many concepts associated with OO such as polymorphism and overloading.
What Go does have are structures, which can be associated with methods. Go also supports a simple but effective form of composition. Overall, it results in simpler code, but there’ll be occasions where you’ll miss some of what OO has to offer.
Structs
A struct
is a collection of fields. And a type
declaration associates the name with the struct.
type Vertex struct {
X int
Y int
}
Struct fields are accessed using a dot.
v0 := Vertex {
X: 1,
Y: 2, // This comma is required
}
v1 := Vertex{1, 2}
v2 := Vertex{X: 1} // Y:0 is implicit
v3 := Vertex{} // X:0 and Y:0
v1.X = 4
We don’t have to set all or even any of the fields when creating a value of the structure. Unassigned fields get a zero value.
Despite the lack of constructors, Go does have a built-in new
function which is used to allocate the memory required by a type. The result of new(X)
is the same as &X{}
.
Getters and Setters
Go doesn’t provide automatic support for getters and setters. If you have a field called owner
(lower case, unexported), the getter method should be called Owner
(upper case, exported), not GetOwner
. The use of upper-case names for export provides the hook to discriminate the field from the method. A setter function, if needed, will likely be called SetOwner
.
Anonymous Fields
It is possible to create structs with fields which contain only a type without the field name. These kind of fields are called anonymous fields. Even though an anonymous fields does not have a name, by default the name of a anonymous field is the name of its type. For example,
type Person struct {
string
int
}
has two fields with name string
and int
.
Pointer
The type *T
is a pointer to a T
value. Its zero value is nil
.
var p *int
The &
operator generates a pointer to its operand (it’s called the address of operator).
i := 42
p = &i
The *
operator denotes the pointer’s underlying value.
fmt.Println(*p) // read i through the pointer p
*p = 21 // set i through the pointer p
Unlike C, Go has no pointer arithmetic.
To access the field X
of a struct when we have the struct pointer p
we could write (*p).X
. However, the language permits us instead to write just p.X
.
Pointer v.s. Value
As you write Go code, it’s natural to ask yourself should this be a value, or a pointer to a value? There are two pieces of good news. First, the answer is the same regardless of which of the following we’re talking about:
- A local variable assignment
- Field in a structure
- Return value from a function
- Parameters to a function
- The receiver of a method
Secondly, if you aren’t sure, use a pointer.
Method
Go does not have classes. However, you can define methods on types. A method is a function with a special receiver argument. The receiver appears in its own argument list between the func
keyword and the method name.
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
A method is just a function with a receiver argument.
You can only declare a method with a receiver whose type is defined in the same package as the method. You cannot declare a method with a receiver whose type is defined in another package (which includes the built-in types such as int).
Pointer receivers
You can declare methods with pointer receivers. This means the receiver type has the literal syntax *T
for some type T
. (Also, T
cannot itself be a pointer such as *int
.)
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
Methods with pointer receivers can modify the value to which the receiver points. Since methods often need to modify their receiver, pointer receivers are more common than value receivers. With a value receiver, the method operates on a copy of the original value. This is the same behavior as for any other function argument.
Methods with pointer receivers can be called on values directly, i.e. v.Scale(5)
is the same as (&v).Scale(5)
when Scale
has a pointer receiver. Methods with value receivers take either a value or a pointer as the receiver when they are called, i.e. p.Abs()
is interpreted as (*p).Abs()
.
Pointer receivers are more convenient. The first reason is so that the method can modify the value that its receiver points to. The second is to avoid copying the value on each method call. In general, all methods on a given type should have either value or pointer receivers, but not a mixture of both.
Composition
Go supports composition, which is the act of including one structure into another. In some languages, this is called a trait or a mixin. Composition can be achieved in Go is by embedding one struct type into another. For example,
type author struct {
firstName string
lastName string
bio string
}
func (a author) fullName() string {
return fmt.Sprintf("%s %s", a.firstName, a.lastName)
}
type post struct {
title string
content string
author
}
func (p post) details() {
fmt.Println("Title: ", p.title)
fmt.Println("Content: ", p.content)
fmt.Println("Author: ", p.author.fullName())
fmt.Println("Bio: ", p.author.bio)
}
Note the anonymous field author
in the post
struct. This field denotes that post
struct is composed of author
. Now post
struct has access to all the fields and methods of the author
struct. Thus you can do p.fullName()
besides p.author.fullName()
. However, post
struct can always “overwrite” the fullname()
method defined for author
:
func (p post) fullName() string {
return fmt.Sprintf("Author: %s %s", p.author.firstName, p.author.lastName)
}