Go: functions, methods, pointers and interfaces

Β· 2911 words Β· 14 minute read

This article was created especially for my son, who had hard times understanding using pointers and interfaces.

To make the explanation complete, I added (with his help) two things: functions and methods. I see those two as a complement to pointers and/or interfaces. Or vice versa.

This explanation is divided into six parts, each part adding onto previous part. I tried to make it all as simple as possible.

The basics πŸ”—

We will start with something very simple. We are creating two structs: Car and Truck. Both have one field name.

In a code, we will create two variables, namely bmw and volvo, with a field name. And then we will just print that variable name.

package main

import "fmt"

type Car struct {
 name string
}

type Truck struct {
 name string
}

func main() {
 bmw := Car{name: "bmw is the best car"}
 volvo := Truck{name: "volvo is the best truck"}

 fmt.Println(bmw.name)
 fmt.Println(volvo.name)
}

As you can see, nothing very special here. Hopefully this is the result you will see, when running the code.

bmw is the best car
volvo is the best truck

Changing the name with a function, passing a value πŸ”—

Let’s say we want to change the name field and again, print the result. We have multiple options, how to do this and in this particular example, we will use simple function with “passing the value”, or “passing the whole struct” in this case.

As you can see, we need to create two functions, namely changeNameOfCar() and changeNameOfTruck().

In the code (lines 17, 18) we are changing the name fields for those two variables using those new functions.

Those two functions are returning the name with an added information "\n\t… changed with simple function".

package main

import "fmt"

type Car struct {
 name string
}

type Truck struct {
 name string
}

func main() {
 bmw := Car{name: "bmw is the best car"}
 volvo := Truck{name: "volvo is the best truck"}

 bmw.name = changeNameOfCar(bmw)
 volvo.name = changeNameOfTruck(volvo)

 fmt.Println(bmw.name)
 fmt.Println(volvo.name)
}

func changeNameOfCar(c Car) string {
 return c.name + "\n\t... changed with simple function"
}

func changeNameOfTruck(t Truck) string {
 return t.name + "\n\t... changed with simple function"
}

If you run the code, this is the result you will see.

bmw is the best car
        ... changed with simple function
volvo is the best truck
        ... changed with simple function

As you can observe, we successfully changed the field name for variables bmw and volvo by invoking a proper function and passing the value to it.

In this case, we passed the whole variable bmw and volvo. And inside those two functions changeNameOfCar() and changeNameOfTruck() we are working with a copy of that bmw and volvo variable.

To make the change to that original variable, we need to return that changed name back and assign it to original bmw.name and volvo.name.

The “problem” with previous approach is a “memory” based one. If you have a huge struct with million of fields, it would not be wise to pass the variable with all million fields, into that function. The first approach is to pass just the fields you want to change, like bmw.name, the second approach is to pass a pointer to that variable.

3. Changing the name with a function, using a pointer πŸ”—

First of all, what is a pointer?

Imagine this. You have a car, that BMW. That is a physical object. And that car is parked somewhere. And that somewhere has an address, like Street12, City 123.

And that address is a pointer, that points to a physical location of that car.

From that perspective, if you have you car parked home at that address and you want someone to just check, if the car is ok (because you are on a vacation), you can simply tell to someone β€œcan you please check, that my car at Street 12, City 123 is really there?”. That Street12, City 123 is a pointer to your car. And the place, where you car is physically, well, that is a physical place in the universe.

If you have a variable bmw, that physically occupies a place in your computers memory, you also have an address, a pointer to that place.

That means: if you have a variable with million of fields and you want to use and/or change that variable with a function, you can simply pass a pointer to a location in memory, location of that variable. No need to pass the whole variable.

Here is the code with two new functions on lines 19–20.

package main

import "fmt"

type Car struct {
 name string
}

type Truck struct {
 name string
}

func main() {
 bmw := Car{name: "bmw is the best car"}
 volvo := Truck{name: "volvo is the best truck"}

 bmw.name = changeNameOfCar(bmw)
 volvo.name = changeNameOfTruck(volvo)
 changeNameOfCarUsingPointer(&bmw)
 changeNameOfTruckUsingPointer(&volvo)

 fmt.Println(bmw.name)
 fmt.Println(volvo.name)
}

func changeNameOfCar(c Car) string {
 return c.name + "\n\t... changed with simple function"
}

func changeNameOfTruck(t Truck) string {
 return t.name + "\n\t... changed with simple function"
}

func changeNameOfCarUsingPointer(c *Car) {
 c.name = c.name + "\n\t... changed using pointer"
}

func changeNameOfTruckUsingPointer(t *Truck) {
 t.name = t.name + "\n\t... changed using pointer"
}

These two new functions changeNameOfCarUsingPointer() and changeNameOfTruckUsingPointer(). And as you can see, you are not passing variables bmw and volvo into those functions, but you are passing just the pointers &bmw and &volvo. Just the address to a memory location, where those two variables are sitting. Nothing more.

Because you are passing the pointer to your variable, you are working directly with that original variable, inside those two new functions, not with a copy!

That means there is no need to changing back that bmw.name and volvo.name, as we did on lines 17–18. And that also means there is no need to return something back from those two new functions.

By passing a value, you are working with a copy of your original variable. To make a change to that original variable, you need to send what is changed back from the function.

By passing a pointer, you are working with the memory location of that original variable. Any changes you make in this function will affect the original variable.

Maybe you are asking, which approach is better. Passing the value, or passing the pointer? And the answer is: neither. Both have they pros and cons.

My personal simple approach is this: if the variable is small (from the memory perspective), like that volvo and bmw variable, I will always pass the value. To be sure that function will not affect the original variable. Let’s say this is my safety perspective.

But if I have a big variable, like a 10MB variable with millions of fields (an example, please), and I want to change a lot of those fields, I will pass the pointer, because I don’t want to make another 10MB copy of that variable in memory.

If you run the code above, you will see the result.

bmw is the best car
 … changed with simple function
 … changed using pointer
volvo is the best truck
 … changed with simple function
 … changed using pointer

4. Changing the name with a method, passing a value πŸ”—

At first, we need to make a distinction between method and function. The difference is mainly syntactic, but there is also a subtle functional difference, as you will see later.

If you use a function to work with a struct variable, the syntax is like this:

nameOfFunction(variable)

If you use a method to work with a struct variable, the syntax is like this:

variable.nameOfMethod()

As you can see, from the text perspective, we just switched the position of the variable and the name of function/method. And generally, that is really what is the difference, because everything you can do with a function, you can do with a method. But as I wrote, there is a small difference, as you will see.

Now we will use a method to change the variable name, by passing the value. See the code below.

package main

import "fmt"

type Car struct {
 name string
}

type Truck struct {
 name string
}

func main() {
 bmw := Car{name: "bmw is the best car"}
 volvo := Truck{name: "volvo is the best truck"}

 bmw.name = changeNameOfCar(bmw)
 volvo.name = changeNameOfTruck(volvo)
 changeNameOfCarUsingPointer(&bmw)
 changeNameOfTruckUsingPointer(&volvo)
 volvo.name = volvo.changeNameUsingMethod()
 bmw.name = bmw.changeNameUsingMethod()

 fmt.Println(bmw.name)
 fmt.Println(volvo.name)
}

func changeNameOfCar(c Car) string {
 return c.name + "\n\t... changed with simple function"
}

func changeNameOfTruck(t Truck) string {
 return t.name + "\n\t... changed with simple function"
}

func changeNameOfCarUsingPointer(c *Car) {
 c.name = c.name + "\n\t... changed using pointer"
}

func changeNameOfTruckUsingPointer(t *Truck) {
 t.name = t.name + "\n\t... changed using pointer"
}

func (c Car) changeNameUsingMethod() string {
 return c.name + "\n\t... changed with method"
}

func (t Truck) changeNameUsingMethod() string {
 return t.name + "\n\t... changed with method"
}

Those two new methods are on lines 21–22. And when you check their implementations on lines 44–50 you can observer the implementation is the same like for a function, with passing the value.

From this perspective, the functionality is the same and when you run the code, you will see the result.

bmw is the best car
        ... changed with simple function
        ... changed using pointer
        ... changed with method
volvo is the best truck
        ... changed with simple function
        ... changed using pointer
        ... changed with method

But now about those differences.

Our first function, we wrote, was changeNameOfCar() and we passed the whole volvo variable in it, so it looked and functioned like changeNameOfCar(volvo). But there is no need to pass the whole variable, you can make a function like changeNameOfCar(volvo.name) and pass just the volvo.name into that function (and of course, you have to properly adjust the function).

But by using method volvo.changeNameUsingMethod there is no way you can pass only name, you cannot use it like volvo.name.changeNameUsingMethod().

By using method, you are working on/with the whole variable. And because we are passing the value, you are working on/with a NEW COPY of that variable. Like with a function, we need to return something back and assign that something back to the original.

Maybe you already spot another difference. With functions, you cannot generally use the same name for two different types of variables. You cannot use changeName() for both Truck and Car.

But, as you can see, you can use the same method name for different types of variables. But you need to implement this method for both variables (lines 44–50). Two methods, same name, different type on input.

So you can have volvo.ChangeName(), bmw.ChangeName(), elonMusk.ChangeName() and/or Quercus.ChangeName(). One name for four different methods.

This will be useful, when we will be dealing with interfaces later on.

5. Changing the name with a method, passing a pointer πŸ”—

By now, you hopefully understand the difference between passing a value and passing a pointer and between function and a method.

There is one last piece missing: using a method and passing a pointer. It’s like using the best of those two worlds.

Here is the updated code, with those two new functions on lines 23–24.

package main

import "fmt"

type Car struct {
 name string
}

type Truck struct {
 name string
}

func main() {
 bmw := Car{name: "bmw is the best car"}
 volvo := Truck{name: "volvo is the best truck"}

 bmw.name = changeNameOfCar(bmw)
 volvo.name = changeNameOfTruck(volvo)
 changeNameOfCarUsingPointer(&bmw)
 changeNameOfTruckUsingPointer(&volvo)
 volvo.name = volvo.changeNameUsingMethod()
 bmw.name = bmw.changeNameUsingMethod()
 bmw.changeNameUsingMethodWithPointer()
 volvo.changeNameUsingMethodWithPointer()

 fmt.Println(bmw.name)
 fmt.Println(volvo.name)
}

func changeNameOfCar(c Car) string {
 return c.name + "\n\t... changed with simple function"
}

func changeNameOfTruck(t Truck) string {
 return t.name + "\n\t... changed with simple function"
}

func changeNameOfCarUsingPointer(c *Car) {
 c.name = c.name + "\n\t... changed using pointer"
}

func changeNameOfTruckUsingPointer(t *Truck) {
 t.name = t.name + "\n\t... changed using pointer"
}

func (c Car) changeNameUsingMethod() string {
 return c.name + "\n\t... changed with method"
}

func (t Truck) changeNameUsingMethod() string {
 return t.name + "\n\t... changed with method"
}

func (c *Car) changeNameUsingMethodWithPointer() {
 c.name = c.name + "\n\t... changed with method using pointer"
}

func (t *Truck) changeNameUsingMethodWithPointer() {
 t.name = t.name + "\n\t... changed with method using pointer"
}

The code is almost the same, but because we are passing the pointer, there is no need to return something back from the method and assign those something to the original variable.

Also, we are not using (t Truck) in the implementation, but we are using the pointer, namely (t *Truck). Please be sure to always check this when using methods, because there is nothing like passing a pointer &variable when using a method, like in changeNameOfCarUsingPointer(&bmw). There is no & while using a method.

Run the code and see the result.

bmw is the best car
        ... changed with simple function
        ... changed using pointer
        ... changed with method
        ... changed with method using pointer
volvo is the best truck
        ... changed with simple function
        ... changed using pointer
        ... changed with method
        ... changed with method using pointer

Intermezzo πŸ”—

By now we have four possible ways how to deal with a problem, changing a name of a variable, in this case:

  1. Function, value (safer, but copying the data)
  2. Function, pointer (not safer β€” you are changing the original, but not copying the data)
  3. Method, value (the same like function, but you are always copying the whole variable, you can use the same name for different types of variables)
  4. Method, pointer (the same like with function and pointer, and like with previous, you can use the same name over and again)

6. Using an interface πŸ”—

Let’s say you want to make a type Vehicle with “subtypes” like Car, Truck, Motorcycle and others. And the reason, you want to make this type Vehicle is this: somewhere in the code, you want to make an array of all those vehicles and change their names in one loop, using already written methods for Car and Truck.

Like in the code below, where we are creating an array of vehicles and adding already created volvo and bmw.

var vehicles []Vehicle
vehicles = append(vehicles, &bmw)
vehicles = append(vehicles, &volvo)

for _, vehicle := range vehicles {
   vehicle.changeNameUsingMethodWithPointer()
}

The problem is, there is no type Vehicle in our code. The second problem is… how can we combine our already written methods for Car and Truck with that non-existent Vehicle.

If you insert that code, you will get red squiggly lines, marking there is a problem.

Go’s interface to the rescue.

package main

import "fmt"

type Car struct {
 name string
}

type Truck struct {
 name string
}

type Vehicle interface {
 changeNameUsingMethodWithPointer()
}

func main() {
 bmw := Car{name: "bmw is the best car"}
 volvo := Truck{name: "volvo is the best truck"}

 bmw.name = changeNameOfCar(bmw)
 volvo.name = changeNameOfTruck(volvo)
 changeNameOfCarUsingPointer(&bmw)
 changeNameOfTruckUsingPointer(&volvo)
 volvo.name = volvo.changeNameUsingMethod()
 bmw.name = bmw.changeNameUsingMethod()
 bmw.changeNameUsingMethodWithPointer()
 volvo.changeNameUsingMethodWithPointer()

 var vehicles []Vehicle
 vehicles = append(vehicles, &bmw)
 vehicles = append(vehicles, &volvo)

 for _, vehicle := range vehicles {
  vehicle.changeNameUsingMethodWithPointer()
 }

 fmt.Println(bmw.name)
 fmt.Println(volvo.name)
}

func changeNameOfCar(c Car) string {
 return c.name + "\n\t... changed with simple function"
}

func changeNameOfTruck(t Truck) string {
 return t.name + "\n\t... changed with simple function"
}

func changeNameOfCarUsingPointer(c *Car) {
 c.name = c.name + "\n\t... changed using pointer"
}

func changeNameOfTruckUsingPointer(t *Truck) {
 t.name = t.name + "\n\t... changed using pointer"
}

func (c Car) changeNameUsingMethod() string {
 return c.name + "\n\t... changed with method"
}

func (t Truck) changeNameUsingMethod() string {
 return t.name + "\n\t... changed with method"
}

func (c *Car) changeNameUsingMethodWithPointer() {
 c.name = c.name + "\n\t... changed with method using pointer"
}

func (t *Truck) changeNameUsingMethodWithPointer() {
 t.name = t.name + "\n\t... changed with method using pointer"
}

Above is the full code and as you can see, aside from those 7 lines above (array and loop) we added just three new lines on lines 13-15:

type Vehicle interface {
   changeNameUsingMethodWithPointer()
}

Those three new lines create an interface named Vehicle. Treat it like a “main type” for all your standard structs, car and truck in this case. This interface has only one method changeNameUsingMethodWithPointer(). And we already have this method implemented, both for Car and for Truck.

That means, you can treat bmw and volvo as a Vehicle now.

That also means, you can make an array of different type of variables and you can treat all those different types as one kind, namely Vehicle, in this case.

If you run the code now, you can see that our method using a pointer was called twice for bmw and twice for volvo. The first time on lines 27–28 and the second time in a loop on line number 35.

bmw is the best car
        ... changed with simple function
        ... changed using pointer
        ... changed with method
        ... changed with method using pointer
        ... changed with method using pointer
volvo is the best truck
        ... changed with simple function
        ... changed using pointer
        ... changed with method
        ... changed with method using pointer
        ... changed with method using pointer

Summary πŸ”—

This is a proper time to stop and let you experiment. No need to make this article anymore complicated.

You can play with passing values and pointers, all at once.

You can play with returning more than one value back.

You can play with Go’s reflect.

You can combine more things together, and end up with bmw.ChangePatrametersTo(name: "My brand new car", date: "14.8.2021", diary: &allCarsIHave).

You can do a lot.

source of this blog post : https://itnext.io/go-functions-methods-pointers-and-interfaces-1c034fd198d3

Share:
waffarx cash back