There’s a good chance you’re coming from another programming language while reading this and if that’s true, it must be explicitly said:

Go does not support Object-Oriented Programming (OOP). Sorry not sorry ๐Ÿ™ƒ I say that because it has been stated (and argued ๐Ÿ˜‚)

time - and - time - again

That inheritance belongs in the bin ๐Ÿ—‘๏ธ Now don’t get me wrong

  • Abstraction? โœ… We got that
  • Polymorphism? โœ… We got that
  • Encapsulation? โœ… We got that
  • Inheritance? โŒ ๐Ÿ™… No we have something better – Composition

Many principles that make OOP are great ๐Ÿ˜๐Ÿ‘ and just because your language supports Inheritance, doesn’t mean you have to use it. It’s just the glaring fault that inheritance brings. Much like a chair ๐Ÿช‘ That has a half broken leg. Are you going to sit on it? ๐Ÿ˜ But that’s OK, because Go does better! We don’t have to look into the future ๐Ÿ”ฎ, we don’t have to break encapsulation and we don’t have to share unnecessary state or methods ๐Ÿ˜ฌ And better yet! We get to have our cake๐Ÿฐ and eat it too because if you are familiar with OOP, the style can be mimicked in Go. So don’t fret my OOP friends you are welcome and have a place here ๐Ÿฅฐ and the first step is introducing methods.

Speaking of methods, what is a method? ๐Ÿค” If you’re brand new to programming, Welcome! ๐Ÿ‘‹๐Ÿ˜ I hope you have as much fun as I do engineering solutions to my clients problems (that client sometimes being myself ๐Ÿ˜‚). Now back to the question.

What’s a method? A method is identical to a function. It just changes the star of the show ๐ŸŒŸ to the struct that you pass in. Functional programming revolves around inputs going to outputs and chaining ๐Ÿ”—๐Ÿ”—๐Ÿ”—๐Ÿ”— them together. Where as OOP revolves around a struct, in the case of Go, and updating the fields (also known as updating encapsulated state) of that struct while having inputs interact with it; there may or may not be an output.

Setup

Let’s make our directory method and the files we want inside of that directory example_test.go method.go

mkdir method
touch method/example_test.go method/method.go

Now let’s open up method.go and for the very first line we’ll add

package method

Next for example_test.go for the very first line we’ll add

package method_test

We can import basics/method into cmd/main.go and run functions from there with go run cmd/main.go and also to run our example_test.go ๐Ÿ‘ we use go test method/example_test.go in the commandline.

Initial Setup

All of our examples will fail until we do the last part so hold on to your britches partner ๐Ÿค  or run them in the meantime and see what output it expects in comparison to what we will write for each of our tests. It’ll be ๐Ÿ good learning ๐Ÿ˜„

Now we’re going to use a Gopher as a struct like we did in the previous lesson on structs and we’ll create a constructor just the same. Using idiomatic Go naming conventions by calling it New as we already learned about.

Coding Time!

https://twitter.com/egonelbre

method.go

type Gopher struct {
	name      string
	isCoding  bool
	favNumber int
}

// New returns a constructed Gopher of the given values.
func New(name string, isCoding bool, favNumber int) *Gopher {
	return &Gopher{name: name, isCoding: isCoding, favNumber: favNumber}
}

example_test.go

func ExampleNew() {
	g := method.New("Ghidra", false, 0xBEEF)
	fmt.Print(g)
	// Output: Hi! I'm Ghidra and my favorite number is 48879
}

Pointer Receiver ๐Ÿ†š Value Receiver

Now on to the meat ๐Ÿฅฉ and ๐Ÿฅ” potatoes of this lesson. Methods! There are two types of methods:

  1. Pointer Receiver Methods
  2. Value Receiver Methods

As we learned in the last lesson pointers change things, and when you pass by value you make a copy of that thing.

Said again a method is making our Gopher the star of the show https://twitter.com/egonelbre because we’re focusing on them and the inputs are secondary to our Gopher.

Now why would we want to change our Gopher? (Pointer Receiver) and why would we only pass in a copy of our Gopher? (Value Receiver)

Short answer: It’s a signal to you and anyone reading your code.

Value Receiver Method

Remember when something is const – We’re done. Brain ๐Ÿง  shut off. We don’t have to EVER worry about it changing, ever. That’s so nice. Set it ๐Ÿง‘โ€๐Ÿณ and forget it. Meaning when a method is a value receiver you can know with ๐Ÿ’ฏ % certainty that it will not change the struct. Therefore when you are sure that you don’t need to change anything in the struct you should use a value receiver method.

Pointer Receiver Method

These are much more typical and closer to what OOP strives for. If you are taking in inputs to change the underlying struct we must use a pointer receiver method. This trips up new developers all the time when their struct is not changing, with the method they put on it. So let’s go see exactly what happens! ๐Ÿƒ

Coding Time!

https://twitter.com/egonelbre

method.go

// DoesNotChangeFavNumber is a value receiver method that does not change the
// favNumber of the Gopher.
func (g Gopher) DoesNotChangeFavNumber(favNumber int) {
	// Since `g` is a copy and not a pointer `*`, the favNumber we update is
	// specific to this copy of `g`.
	// The original `g` still has its favNumber intact.
	g.favNumber = favNumber
}

// DoesChangeFavNumber is a pointer receiver method that will change the
// favNumber of the Gopher.
func (g *Gopher) DoesChangeFavNumber(favNumber int) {
	// We can drop the dereference to the pointer `(*g)` to just `g` if we wanted
	// because Go will automagically ๐Ÿช„ dereference pointer values for us!
	(*g).favNumber = favNumber
	// This works too and is preferred:
	// g.favNumber = favNumber
}

example_test.go

func ExampleGopher_DoesChangeFavNumber() {
	g := method.New("Gorple", false, 99)
	g.DoesChangeFavNumber(800)
	fmt.Println(g)
	g.DoesChangeFavNumber(-28298)
	fmt.Println(g)
	g.DoesChangeFavNumber(0xDEAD)
	fmt.Println(g)
	g.DoesChangeFavNumber(0xC0FFEE)
	fmt.Println(g)
	// Output:
	// Hi! I'm Gorple and my favorite number is 800
	// Hi! I'm Gorple and my favorite number is -28298
	// Hi! I'm Gorple and my favorite number is 57005
	// Hi! I'm Gorple and my favorite number is 12648430
}

func ExampleGopher_DoesNotChangeFavNumber() {
	g := method.New("Galum", false, 99)
	g.DoesNotChangeFavNumber(800)
	fmt.Println(g)
	g.DoesNotChangeFavNumber(-28298)
	fmt.Println(g)
	g.DoesNotChangeFavNumber(0xDEAD)
	fmt.Println(g)
	g.DoesNotChangeFavNumber(0xC0FFEE)
	fmt.Println(g)
	// Output:
	// Hi! I'm Galum and my favorite number is 99
	// Hi! I'm Galum and my favorite number is 99
	// Hi! I'm Galum and my favorite number is 99
	// Hi! I'm Galum and my favorite number is 99
}

Methods ๐Ÿ†š Functions

In Go you cannot overload functions. For example a simple add function func Add(x, y int) int cannot be redeclared as func Add(x, y float64) float 64. The complier will complain that the function was “redeclared in this block”. So what happens if we declare a method and a function with the same name and parameters? ๐Ÿค”

We’ll find out when we get to coding, but first let’s talk ๐Ÿ—ฃ๏ธ about methods and functions one more time. For some people it’s easier to see most, if not everything as a thing. For example a dog ๐Ÿ• or a frog ๐Ÿธ maybe some fog ๐ŸŒซ๏ธ and they want to tell stories about those things. The Dog.Bark() or the Frog.Jump() or Fog.Appear() this is perfect for methods then. Methods exist on that type. The story ๐Ÿ“– is driven by these objects.

For other people it’s easier to see cut โœ‚๏ธ and dry ๐Ÿœ๏ธ inputs to output. The action performed and the result of each function creates the story FindFirst(dog, HasCollar), Collect(frogs, NotFullBucket), Test(fog, WithHygrometer) this is perfect for functions and even better in Go as functions are first class and allow for higher-ordered functions. We can see that in action as we pass a supporting function into the above functions. ๐Ÿ‘†

For other people they use the right tool ๐Ÿชš for the right job ๐Ÿชต. Sometimes a method makes more sense other times a function does and Go is flexible in the regard that it allows you to program for your situation. Even when you want to have the same name and parameters for a method to a struct and to a function ๐Ÿ˜‰

After you code up the below examples, think of which makes more sense for the situation.

Coding Time!

https://twitter.com/egonelbre

method.go

// StartCoding is a pointer receiver method that starts the Gopher to coding.
func (g *Gopher) StartCoding() { g.isCoding = true }

// StopCoding is a pointer receiver method that stops the Gopher from coding.
func (g *Gopher) StopCoding() { g.isCoding = false }

// StartCoding is a package function that starts the Gopher to coding.
func StartCoding(g *Gopher) { g.isCoding = true }

// StopCoding is a package function that stops the Gopher from coding.
func StopCoding(g *Gopher) { g.isCoding = false }

example_test.go

func ExampleStartCoding() {
	g := method.New("Geany", false, 3333)
	method.StartCoding(g)
	fmt.Println(g)
	// Output:
	// Let me get back to you after I'm done coding.
}

func ExampleStopCoding() {
	g := method.New("Ghjil", true, 0b010101011110111111)
	method.StopCoding(g)
	fmt.Println(g)
	// Output:
	// Hi! I'm Ghjil and my favorite number is 87999
}

func ExampleGopher_StartCoding() {
	g := method.New("Garkicye", true, 0xBA5ED)
	g.StartCoding()
	fmt.Println(g)
	// Output:
	// Let me get back to you after I'm done coding.
}

func ExampleGopher_StopCoding() {
	g := method.New("Gremeri", true, 87999)
	g.StopCoding()
	fmt.Println(g)
	// Output:
	// Hi! I'm Gremeri and my favorite number is 87999
}

Pretty Printing with String()

A feather ๐Ÿชถ light introduction to the next lesson, interfaces. There exists in the fmt package a very useful interface known as Stringer the docs (the documentation) say

The String method is used to print values passed as an operand to any format that accepts a string or to an unformatted printer such as Print

but don’t take my word for it, open up your terminal and put go doc fmt.Stringer and see what you get ๐Ÿ˜€

The thing about an interface is it only defines behavior, and has no implementation. Which means they are impossible to test because they don’t do something they define something. So congratulate ๐Ÿฅณ๐ŸŽŠ๐Ÿฅณ yourself after you code this up because you will have implemented your very first interface! ๐ŸŽ‰๐ŸŽ‰

I wanted to show off where a real world ๐ŸŒ example of a value receiver is useful. In order to satisfy the Stringer interface you must use a value receiver. And now you’ll know how to make your struct pretty print. That is to say, have a nice formatted message that’s easy to understand.

Coding Time!

https://twitter.com/egonelbre

method.go

// String satisfies the fmt.Stringer interface and will be used anytime we try
// to fmt.Print(g), fmt.Printf(g), fmt.Println(g) -- g == Gopher
// We can see it is a value receiver method, meaning it takes a copy of a
// Gopher. This is because we **don't** want to update any values of our
// original Gopher and by using a value receiver method we make sure that
// cannot happen.
func (g Gopher) String() string {
	if g.isCoding {
		return "Let me get back to you after I'm done coding."
	} else {
		return fmt.Sprint(
			"Hi! I'm ", g.name, " and my favorite number is ", g.favNumber)
	}
}

example_test.go

func ExampleGopher_String() {
	fmt.Println(method.New("Gasitti", false, 42))
	fmt.Println(method.New("Gon", true, 42))
	// Output:
	// Hi! I'm Gasitti and my favorite number is 42
	// Let me get back to you after I'm done coding.
}

Source File ๐Ÿ“„

The Source File

Test File ๐Ÿ“

The Test File