GoLang methods

by sagacity

Functions in GoLang have great ability to return multiple values. For a C programmer, this may sound as something weird. But with a quick experimentation, the ideas gets clear. How does GoLang orchestrate the multiple returning values is bit out-of-scope of this blog post, however, I’ll try to explain that aspect too in one of the blogs in the due course of time ahead.

Methods is GoLang should be understood as an extension to functions. A caller when invokes a function passes on certain things for the function to work on. We all know that “something” is a set of formal arguments. These formal arguments are treated as stack frames of the called function. They remain with the function and their scope is limited in the jurisdiction of the function. I believe, this isn’t new to those who know programming. Method in GoLang, as we’ve said earlier, is a kind of extension to function. This’s in essence that a method, apart from just the formal arguments that are optional, also have a receiver type associated with the same. The receiver can be a variable of any user defined type. Everything else remains the same as that with functions. So, if that’s the case, what’s the big deal with methods? What is so different with methods? Why do we need to learn an additional idiom? All these and many other such questions will be answered in the blog series on “GoLang interfaces” in times ahead. So, for now, just make it a point to learn methods in GoLang rather than arguing what is and what is not. And for the sake of argument, methods form the basis for interfaces.

Let’s get on. So, first thing first, the method has the following syntax:

func (var T) methodName([arg1 T,...]) [T1, ...]

Where T is any user defined type

Let's directly jump to a very simple example to understand this:
==
// example-1: Demonstrates simple methods. Also explains the referencing and dereferencing principle.
package main

import (
	"fmt"
)


type device struct {
	id int
	mac string
}

type T int

func (dev device) set() {
	dev.id = 10
	dev.mac = "00:00:00:00:00:01"
}

func (pDev *device) pset() {
	pDev.id = 10
	pDev.mac = "00:00:00:00:00:02"
}

func (dev device) get(name string) {
	fmt.Printf("%s.id  : %d\n", name, dev.id)
	fmt.Printf("%s.mac : %s\n", name, dev.mac)
}


func (pI *T) set(n int) {
	*pI = T(n)
}
func (pI *T) get(name string) {
	fmt.Printf("%s: %d\n", name, int(*pI))
}

func (dev device) test(name string) {
	fmt.Printf("test:  %s: id : %d\n", name, dev.id)
	fmt.Printf("test:  %s: mac: %s\n", name, dev.mac)
}

func main() {
	dev1 := device{}
	pDev2 := &device{}

	dev1.set()
	dev1.get("dev1")
	pDev2.pset()
	pDev2.get("pDev1")
	(*pDev2).test("pDev2")

	var t T
	(&t).set(10)
	(&t).get("t")

	var t1 T
	t1.set(20)
	t1.get("t1")

	fmt.Printf("t1 with int: %d\n", int(t1))
	fmt.Printf("t1 with T  : %d\n", t1)
}

In the above example, get() method expects receiver to be a pointer to type T. However, variable “t” in call t.get() isn’t a pointer. GoLang methods does the automatic referencing and dereferencing. Conversion happens according to the receiver in the method header.

We use identifier names in resemblance with the purpose they serve. This’s perfectly applicable for function names too. Assume there’ve different types that resemble 2 dimensional geometry shapes, for instance, circle, rectangle, square. We need to compute area and perimeter of each. We use function identifier names computeArea() and compute perimeter() for the respective purpose.

One way is to write separate functions for computeArea and perimeter. For instance, area circle, area square, and so on, and perimeter circle, perimeter square, and so on and so forth. So, this means, we’ll define 8 functions, 4 for computing area, and 4 for the computePerimeter.

Note: So far, we haven’t talked anything about interfaces, so it’s not going to be worthwhile to introduce a concept called empty interfaces, i.e., interface{} for now.

Another way is to define 4 sets of functions, each set consisting of 2 functions namely computeArea() computePerimeter(), where each individual set is fixed on 1 type that are defined above. And thus, each set now works on its own type, the type is called as a receiver.

This means, now there’re four functions with name computeArea() and 4 functions with name computePerimeter().

So, someone may argue, what’s a big deal in it! We anyway define 8 functions separately. The answer is there’s a difference in the notion. The functions, when have a receiver resemble a notion of object orientation. Data members of each type are the attributes of that type and these functions, when called with receiver, now become part of the receiver. This’s a notion of encapsulation, i.e., data and methods of handling those data are treated as 1 unit. These functions, are termed as methods. The difference, though subtle in nature, is remarkable and is a kind of a paradigm shift. It’s exactly the same as we have function-pointer in a structure in C language. In a typical object oriented programming, one would declare these functions inside class definition and define them outside the class definition with a scope resolution operator.

Let’s go through an example:

// example-2.go
// Program demonstrates same method names on different user defined types.

package main

import (
	"fmt"
	"math"
)


type circle struct {
	radius float32
}

type rectangle struct {
	length float32
	breadth float32
}

type square struct {
	side float32
}


func (pC *circle) create(radius float32) {
	pC.radius = radius
}
func (c circle) getData() {
	fmt.Printf("circle:  radius: %.4f,  area: %.4f,  perimeter: %.4f\n", c.radius, c.computeArea(), c.computePerimeter())
}

func (c circle) computeArea() float32 {
	return math.Pi * c.radius * c.radius
}

func (c circle) computePerimeter() float32 {
	return math.Pi * 2.0 * c.radius
}



func (pR *rectangle ) create(length, breadth float32) {
	pR.length, pR.breadth = length, breadth
}
func (r rectangle) getData() {
	fmt.Printf("rectangle:  length: %.4f,  breadth: %.4f,  area: %.4f,  perimeter: %.4f\n", r.length, r.breadth, r.computeArea(), r.computePerimeter())
}

func (r rectangle) computeArea() float32 {
	return r.length * r.breadth
}

func (r rectangle) computePerimeter() float32 {
	return 2.0 * (r.length + r.breadth)
}


func (pS *square) create(side float32) {
	pS.side = side
}
func (s square) getData() {
	fmt.Printf("square:  side: %.4f,  area: %.4f,  perimeter: %.4f\n", s.side, s.computeArea(), s.computePerimeter())
}

func (s square) computeArea() float32 {
	return s.side * s.side
}

func (s square) computePerimeter() float32 {
	return 4.0 * s.side
}


func main() {
	c := circle{}
	r := rectangle{}
	s := square{}

	(&c).create(10.00)
	(&r).create(10.00, 5.00)
	(&s).create(10.00)

	c.getData()
	r.getData()
	s.getData()
}
==

Methods on anonymous fields:
Methods behave exactly like anonymous fields. If an anonymous field implements a method, this method is
available for the type that is using this anonymous field.
Here's a simple example:
==
// example-3.go
// Demonstrates methods on anonymous struct fields.

package main

import (
	"fmt"
)

type ArmyMan struct {
	Name string
	Age int
}

type Cadet struct {
	ArmyMan //an anonymous field of type ArmyMan
	Institute string
}

type Officer struct {
	ArmyMan //an anonymous field of type ArmyMan
	Rank string
}

// ArmyMan method
func (pA *ArmyMan) SayHello() {
	fmt.Printf("Hello, I'm %s.\n", pA.Name)
}

func main() {
	manoj := Cadet{ArmyMan{"Cadet. Manoj Pandey", 19,}, "NDA",}
	sam := Officer{ArmyMan{"Col. Sameer Oak", 43,}, "Colonel",}

	manoj.SayHello()
	sam.SayHello()
}

==


Overriding a method:
In the above example, what if the officer wants to convey about his/her regiment?
Rule for overriding fields on conflicts applies for methods as well. Here's an example:
==
// example-4.go
// Demonstrates how rule of overriding fields on conflicts is applied.

package main

import (
	"fmt"
)

type ArmyMan struct {
	Name string
	Age int
}

type Cadet struct {
	ArmyMan //an anonymous field of type ArmyMan
	Institute string
}

type Officer struct {
	ArmyMan //an anonymous field of type ArmyMan
	Rank string
	Regiment string
}

// ArmyMan method
func (pA *ArmyMan) SayHello() {
	fmt.Printf("Hello, I'm %s.\n", pA.Name)
}

// Officer method
func (pO *Officer) SayHello() {
	fmt.Printf("Hello, I'm %s. I'm commissioned in %s\n", pO.Name, pO.Regiment)
}


func main() {
	manoj := Cadet{ArmyMan{"Cadet Manoj Pandey", 19,}, "NDA",}
	sam := Officer{ArmyMan{"Col. Sameer Oak", 43,}, "Colonel", "9 Para Troopers SpAF",}

	manoj.SayHello()
	sam.SayHello()
}
==

These are some simple examples that would help us understand methods in GoLang. One can now design more expressive programs using methods. In the series “Interfaces in GoLang”, we’ll learn how methods are enhanced further in a more purist form of object oriented approach through Interfaces.

SHARE

Write a response

© 2019 Sagacity. All Rights Reserved. Sagacity logo is registered trademarks of Sagacity Software Pvt. Ltd.