Even Go does not provide classes, we can use structures to create objects as we have learned in the previous tutorial. But in OOP, classes have properties (fields) as well as behaviors (methods) and so far we have only learned about properties of a struct that are structure fields.
💡 Behavior is a action that an object can perform. For example,
Dog
is a type ofAnimal
andDog
canbark
. Hence barking is a behavior of the classDog
. Hence any objects (instances) of the classDog
will have this behavior.
We have seen in the structures lesson, especially in the function field section that a struct field can also be a function. We can add a bark
field of type function which takes no arguments and returns a string woof woof!
. This could be one way to add methods to the struct.
But this does not adhere to the OOP concept as struct
fields do not have any idea of struct
they belong to. Hence methods come to the rescue.
What is a method?
In the previous tutorial, we played with function fields of a struct, hence the concept of method will be very easy for you to understand.
A method is nothing but a function, but it belongs to a certain type. A method is defined with slightly different syntax than a normal function. It required an additional parameter known as a receiver which is a type to which the function belongs. This way, a method (function) can access the properties of the receiver it belongs to (like fields of a struct).
Let’s write a program to get the full name of an Employee
struct using a simple function.
package main
import "fmt"
type Employee struct {
FirstName, LastName string
}
func fullName(firstName string, lastName string) (fullName string) {
fullName = firstName + " " + lastName
return
}
func main() {
e := Employee{
FirstName: "Ross",
LastName: "Geller",
}
fmt.Println(fullName(e.FirstName, e.LastName))
}
// Ross Geller
In the above program, we have created a simple struct type Employee
which has two string fields FirstName
and LastName
. Then we’ve defined the function fullName
which takes two strings arguments and returns a string. The fullname
function returns the full name of an employee by concatenating these two strings.
Then we created a struct e
of type Empoyee
by providing FirstName
and LastName
fields values. To get the full name of the employee e
, we use the fullName
function and provided the appropriate arguments.
This works, but the sad thing is, every time we need to get the full name of an employee (and there could be thousands), we need to pass firstName
and lastName
values to fullName
function manually.
A method can solve this problem easily. To convert a function to the method, we just need an extra receiver parameter in the function definition. The syntax for defining a method is as follows.
func (r Type) functionName(...Type) Type {
...
}
From the above syntax, we can tell that method and function have the same syntax except for one receiver argument declaration (r Type)
just before the function name. Type
is any legal type in Go and function arguments and return values are optional (as usual).
Let’s create fullName
method using the above syntax.
package main
import "fmt"
type Employee struct {
FirstName, LastName string
}
func (e Employee) fullName() string {
return e.FirstName + " " + e.LastName
}
func main() {
e := Employee{
FirstName: "Ross",
LastName: "Geller",
}
fmt.Println(e.fullName())
}
// Ross Geller
In the above program, we have defined fullName
method which does not take any arguments but returns a string. As we can tell from the receiver declaration, this method belongs to Employee
type.
The fullName
method will belong to any object of the type Employee
. Hence that object will automatically get this method as a property. When this method is called on the object (just like a struct field function seen in the previous tutorial), it will receive the object as the receiver e
.
The receiver of the method is accessible inside the method body. Hence we can access e
inside the method body of fullName
. In the above example, since the receiver is a struct of type Employee
, we can access any fields of the struct. Like we did in the previous example, we are concatenating FirstName
and LastName
fields and returning the result.
As a method belongs to a receiver type and it becomes available on that type as a property, we can call that method using Type.methodName(...)
syntax. In the above program, we have used e.fullName()
to get the full name of an employee since fullName
method belongs to Employee
.
💡 This is no different than what we learned in
structs
lesson wherefullName
function was a field of struct. But in case of methods, we don’t have to provide properties of struct because method already knows about them.
Methods with the same name
One major difference between functions and methods is we can have multiple methods with same name while no two functions with the same name can be defined in a package.
We are allowed to create methods with same name as long as their receivers are different. Let’s create two struct types Circle
and Rectangle
and create two methods of the same name Area
which calculates the area of their receiver.
package main
import (
"fmt"
"math"
)
type Rect struct {
width float64
height float64
}
type Circle struct {
radius float64
}
func (r Rect) Area() float64 {
return r.width * r.height
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func main() {
rect := Rect{5.0, 4.0}
cir := Circle{5.0}
fmt.Printf("Area of rectangle rect = %0.2f\n", rect.Area())
fmt.Printf("Area of circle cir = %0.2f\n", cir.Area())
}
// Area of rectangle rect = 20.00
// Area of circle cir = 78.54
In the above program, we have created struct types Rect
and Circle
and created two methods of the same name Area
with receiver type Rect
and Circle
. When we call Area()
method on Rect
and Circle
, their respective methods get executed.
Pointer receivers
So far, we have seen methods belong to a type. But a method can also belong to the pointer of a type.
When a method belongs to a type, its receiver receives a copy of the object on which it was called. To verify that, we can create a method that mutates a struct it receives. Let’s create a method changeName
that changes the name
field of an Employee
struct.
package main
import "fmt"
type Employee struct {
name string
salary int
}
func (e Employee) changeName(newName string) {
e.name = newName
}
func main() {
e := Employee{
name: "Ross Geller",
salary: 1200,
}
// e before name change
fmt.Println("e before name change =", e)
// change name
e.changeName("Monica Geller")
// e after name change
fmt.Println("e after name change =", e)
}
// e before name change = {Ross Geller 1200}
// e after name change = {Ross Geller 1200}
In the above program, we have called method changeName
on struct e
of type Employee
. In the method, we are mutating the name
field value of that struct.
From the above result, we can verify that even we have mutated the receiver object, it did not impact the original object on which the method was called.
This proves that the method changeName
received just a copy of the actual struct e
(from main
method). Hence any changes made to the copy inside the method did not affect the original struct.
But a method can also belong to the pointer of a type. The syntax for method definition which belongs to the pointer of a type is as follows.
func (r *Type) functionName(...Type) Type {
...
}
As you can see from the above definition, the syntax to define a method with a pointer receiver is very similar to the normal method. In the below definition, we instructed Go that this method will belong to the pointer of the Type
instead of the value of the Type
.
When a method belongs to the pointer of a type, its receiver will receive the pointer to the object instead of a copy of the object. Let’s re-write the previous example with a method that receives a pointer receiver.
package main
import "fmt"
type Employee struct {
name string
salary int
}
func (e *Employee) changeName(newName string) {
(*e).name = newName
}
func main() {
e := Employee{
name: "Ross Geller",
salary: 1200,
}
// e before name change
fmt.Println("e before name change =", e)
// create pointer to `e`
ep := &e
// change name
ep.changeName("Monica Geller")
// e after name change
fmt.Println("e after name change =", e)
}
// e before name change = {Ross Geller 1200}
// e after name change = {Monica Geller 1200}
Let’s see what changes we made.
- We changed the definition of the method to receive a pointer receiver using
*Employee
syntax. This way, the receivere
is the pointer to the struct object this method was called on. - Inside the method body, we are converting pointer of the receiver to the value of the receiver using pointer dereferencing syntax (*p). Hence
(*e)
will be the actual value of the struct stored in the memory. - Then we changed the value of the
name
field of structe
. Any change made toe
will be reflected in the original struct. - In the
main
method, we created a pointerep
which points to structe
. - Since the
changeName
method belongs to the pointer of typeEmployee
or type*Employee
, it can be called on value of type*Employee
. - Since the type of
ep
is*Employee
, we can callchangeName
method on it usingep.changeName()
syntax. This will pass the pointerep
to the method as a receiver (instead of valuee
).
💡 In above program, we solely created the pointer
ep
frome
just to call method thechangeName
on it, but you can also use(&e).changeName("Monica Geller")
syntax instead of creating new pointer.
Calling methods with pointer receiver on values
If you are wondering, do I always need to create a pointer to work with methods with pointer receiver then Go already figured that out.
Let’s rewrite the above programming using Go’s shortcuts.
package main
import "fmt"
type Employee struct {
name string
salary int
}
func (e *Employee) changeName(newName string) {
e.name = newName
}
func main() {
e := Employee{
name: "Ross Geller",
salary: 1200,
}
// e before name change
fmt.Println("e before name change =", e)
// change name
e.changeName("Monica Geller")
// e after name change
fmt.Println("e after name change =", e)
}
// e before name change = {Ross Geller 1200}
// e after name change = {Monica Geller 1200}
The above program will work just fine like before. So what changed.
- If a method has a pointer receiver, then you don’t necessarily need to use the pointer dereferencing syntax
(*e)
to get the value of the receiver. You can use simplee
which will be the address of the value that pointer points to but Go will understand that you are trying to perform an operation on the value itself and under the hood, it will converte
to(*e)
. - Also, you don’t necessarily need to call a method on a pointer if the method has a pointer receiver. You are allowed to call this method on the value instead and Go will pass the pointer of the value as the receiver.
💡 You can decide between method with pointer receiver or value receiver depending on your use case. But generally, even if you do not wish to mutate the receiver, methods with pointer receiver are preffered as no new memory is created for operations (in case of methods with value receiver).
Methods on nested struct
We learned a great deal about the nested structure in the previous lesson. As a struct field also can be a struct, we can define a method on parent struct and access nested struct to do anything we want.
package main
import "fmt"
type Contact struct {
phone, address string
}
type Employee struct {
name string
salary int
contact Contact
}
func (e *Employee) changePhone(newPhone string) {
e.contact.phone = newPhone
}
func main() {
e := Employee{
name: "Ross Geller",
salary: 1200,
contact: Contact{"011 8080 8080", "New Delhi, India"},
}
// e before phone change
fmt.Println("e before phone change =", e)
// change phone
e.changePhone("011 1010 1222")
// e after phone change
fmt.Println("e after phone change =", e)
}
// e before phone change = {Ross Geller 1200 {011 8080 8080 New Delhi, India}}
// e after phone change = {Ross Geller 1200 {011 1010 1222 New Delhi, India}}
In the above example, we have defined changePhone
method on *Employee
which receive the pointer of e
. Inside this method, we can access the properties of e
which also contains the nested struct of type Contact
.
Since e
is the pointer in the method, we can mutate the nested struct. In the above example, we have changed the contact
nested struct by mutating the phone
field value.
Methods on nested struct
A nested struct can also have methods. If the inner struct implements a method, then you can call a method on it using .
(dot) accessor.
package main
import "fmt"
type Contact struct {
phone, address string
}
type Employee struct {
name string
salary int
contact Contact
}
func (c *Contact) changePhone(newPhone string) {
c.phone = newPhone
}
func main() {
e := Employee{
name: "Ross Geller",
salary: 1200,
contact: Contact{
phone: "011 8080 8080",
address: "New Delhi, India",
},
}
// e before phone change
fmt.Println("e before phone change =", e)
// change phone
e.contact.changePhone("011 1010 1222")
// e after phone change
fmt.Println("e after phone change =", e)
}
// e before phone change = {Ross Geller 1200 {011 8080 8080 New Delhi, India}}
// e after phone change = {Ross Geller 1200 {011 1010 1222 New Delhi, India}}
Anonymously nested struct
In the previous lesson, we also learned about anonymous fields and field promotions. In a nutshell, if a field of a struct an anonymous struct, the nested struct fields will be promoted to the parent.
Let’s see how we can use the promoted fields inside a method.
package main
import "fmt"
type Contact struct {
phone, address string
}
type Employee struct {
name string
salary int
Contact
}
func (e *Employee) changePhone(newPhone string) {
e.phone = newPhone
}
func main() {
e := Employee{
name: "Ross Geller",
salary: 1200,
Contact: Contact{
phone: "011 8080 8080",
address: "New Delhi, India",
},
}
// e before phone change
fmt.Println("e before phone change =", e)
// change phone
e.changePhone("011 1010 1222")
// e after phone change
fmt.Println("e after phone change =", e)
}
// e before phone change = {Ross Geller 1200 {011 8080 8080 New Delhi, India}}
// e after phone change = {Ross Geller 1200 {011 1010 1222 New Delhi, India}}
As we can see from the above example, since Contact
struct is anonymously nested inside Employee
struct, its fields will be promoted to Employee
and we will be able to access it on the object e
.
Hence any method that accepts a struct receiver will also have access to the promoted fields. Using this principle, we were able to access phone
property of the nested field Contact
on the object e
of type Employee
.
Promoted methods
Like promoted fields, methods implemented by the anonymously nested struct are also promoted to the parent struct. As we saw in the previous example, Contact
field is anonymously nested. Hence we could access phone
field of the inner struct on the parent.
In the same scenario, any method implemented by Contact
struct will be available on Employee
struct. Let’s rewrite the previous example.
package main
import "fmt"
type Contact struct {
phone, address string
}
type Employee struct {
name string
salary int
Contact
}
func (c *Contact) changePhone(newPhone string) {
c.phone = newPhone
}
func main() {
e := Employee{
name: "Ross Geller",
salary: 1200,
Contact: Contact{
phone: "011 8080 8080",
address: "New Delhi, India",
},
}
// e before phone change
fmt.Println("e before phone change =", e)
// change phone
e.changePhone("011 1010 1222")
// e after phone change
fmt.Println("e after phone change =", e)
}
// e before phone change = {Ross Geller 1200 {011 8080 8080 New Delhi, India}}
// e after phone change = {Ross Geller 1200 {011 1010 1222 New Delhi, India}}
We made only one change to changePhone
method. Instead of receiving *Employee
type, this method now expects receiver of type *Contact
. Since fields of the nested struct Contact
are promoted, any method implemented by it will be promoted too. Hence we could call e.changePhone()
as if the type Employee
of struct e
implemented changePhone
method.
💡 However, one thing to remeber here that even we are calling
changePhone()
method one
, the receiver sent by the Go will be of type*Contact
since this method belongs to it.
Methods can accept both pointer and value
When a normal function has a parameter definition, it will only accept the argument of the type defined by the parameter. If you passed a pointer to the function which expects a value, it will not work. This is also true when function accepts pointer but you are passing a value instead.
💡 You should look at this from the perspective of data type. A function which accepts a value of type
Type
hasfunc (arg Type)
parameter definition while a function that accepts a pointer hasfunc (arg *Type)
definition.
But when it comes to methods, that’s not a strict rule. We can define a method with value or pointer receiver and call it on pointer or value. Go does the job of type conversion behind the scenes as we’ve seen in the earlier examples.
package main
import "fmt"
type Employee struct {
name string
salary int
}
func (e *Employee) changeName(newName string) {
e.name = newName
}
func (e Employee) showSalary() {
e.salary = 1500
fmt.Println("Salary of e =", e.salary)
}
func main() {
e := Employee{
name: "Ross Geller",
salary: 1200,
}
// e before change
fmt.Println("e before change =", e)
// calling `changeName` pointer method on value
e.changeName("Monica Geller")
// calling `showSalary` value method on pointer
(&e).showSalary()
// e after change
fmt.Println("e after change =", e)
}
// e before change = {Ross Geller 1200}
// Salary of e = 1500
// e after change = {Monica Geller 1200}
In the above program, we defined changeName
method which receives a pointer but we called on the value e
which is legal because Go under the hood will pass a pointer of e
(of type Employee
) to it.
Also, we defined showSalary
method which receives value but we called it on the pointer to e
which is legal because Go under the hood will pass the value of the pointer (of type Employee
) to it.
💡 We tried to change salary of
e
insideshowSalary
method but did not work as we can see from the result. This is because even we’re calling this method on a pointer, Go will sent only copy of the value to that method.
Methods on non-struct type
So far we have seen methods belonging to struct type but from the definition of the methods, it is a function that can belong to any type. Hence a method can receive any type as long as the type definition and method definition is in the same package.
So far, we defined struct and method in the same main
package, hence it worked. But to verify if we can add methods on foreign types, we will try to add a method toUpperCase
on the built-in type string
.
package main
import (
"fmt"
"strings"
)
func (s string) toUpperCase() string {
return strings.ToUpper(s)
}
func main() {
str := "Hello World"
fmt.Println(str.toUpperCase())
}
// ./prog.go:8:7: cannot define new methods on non-local type string
// ./prog.go:14:18: str.toUpperCase undefined (type string has no field or method toUpperCase)
From the above program, we created the method toUpperCase
which accepts string
as receiver type. Hence we expect string.toUpperCase()
to work and return uppercase version of the receiver s
.
💡 We used
strings
build-in package to convert a string to uppercase.
But the above program will run into a compilation error.
program.go:8: cannot define new methods on non-local type string
program.go:14: str.toUpperCase undefined (type string has no field or method toUpperCase)
This is because of the type string
and method toUpperCase
are not defined in the same package. Let’s create a new derived type MyString
from string
. This way, both the method and a newly defined MyString
type will belong to the same package and it should work.
package main
import (
"fmt"
"strings"
)
type MyString string
func (s MyString) toUpperCase() string {
normalString := string(s)
return strings.ToUpper(normalString)
}
func main() {
str := MyString("Hello World")
fmt.Println(str.toUpperCase())
}
// HELLO WORLD
From the above program, we created the method toUpperCase
which now belongs to MyString
type. We needed to modify the internals of this method to pass string
type to strings.ToUpper
function, but we get it.
Now we can call str.toUpperCase()
because str
is of type MyString
as we used type conversion on the line no. 16 to convert from string
type to MyString
type.