Learning Go – Day 3

Featured image by Egon Elbre

Structs

  • A defined type that is a collection of fields.
  • Structs are value types.
  • Declaration. Note these are known as “named structs” (since you are giving a name to the type).
type Person struct {
	name string
	age int
}

p1 := Person{
	name: "Jannie",
	age: 31, //<-- note the trailing semi colon
}

// without specifying field names
p2 := Person{"Koos", 46}
  • When initializing an instance using field names, you don’t have to specify the values in the declaration order. (e.g. above I could have specified age: 31 and then the name).
  • Anonymous struct declaration
p1 := struct {
	name string
	age int
}{
	name: "Anonymous Person",
	age: -42, //<-- note the trailing semi colon
}
  • Access fields using the dot syntax
fmt.Println(p1.name, p1.age)
  • Zero value of a struct is that each field is initialized with the zero value for the field’s type (e.g. int == 0, string == “”)
  • You can also initialize a struct using the field names and only give values for certain fields (i.e. the unspecified fields will have the zero value)
p4 := Person{name: "Age Zero"}
fmt.Println(p4) // {Age Zero 0}

// Doesn't work when you leave out the field names
p4 := Person{"won't compile"}
  • Pointer to a struct. Note you can dereference the same way as in C: (*p).someField
p5 := &Person{
	name: "Pointer to a Person",
	age:  4200,
}

fmt.Printf("Type: %T\n", p5) // Type: *main.Person 
fmt.Println("Name:", (*p5).name) // Name: Pointer to a Person
// You also don't have to dereference! Nice.
fmt.Println("Name:", p5.name)
  • Structs can also be declared using anonymous fields.
type Person struct {  
    string
    int
}

p6 := Person{
	string: "Sannie",  //<- Note that the field name is by default also the type name
  int:    23,
}
fmt.Println(p6.string)
  • Promoted fields. Basically if you have an anonymous field that is another struct, then you can access fields of this “nested” struct as if the fields belong to the parent struct.
  • Use Pascal casing (i.e. initial Capital letter) to export Structs and field names to be used in other packages.
  • Structs are comparable == if the fields are comparable.

Methods

In most other languages, a method is a function that belongs to a struct or class. Methods in Go are “similar” and is declared by specifying a receiver type between the func keyword and the function name (in this case the method name).

Think of the receiver as “self” or “this” in other languages.

func (receiver Type) methodName(parameters) {  
}

Example

type Person struct {
	name string
	age int
}

func (p Person) sayHello() {
	fmt.Printf("Hello, my name is %s and I am %d years old\n", p.name, p.age)
}

p1 := Person{
	name: "Jannie",
	age:  24,
}

p1.sayHello() // Hello, my name is Jannie and I am 24 years old
  • Remember Go doesn’t support Classes or OOP is you would have learned from other languages. I think this is quite refreshing tbh.
  • The example above uses “value receiver”. This means that since structs are value types, that a copy of the receiver was passed to the method and thus any changes made to the receiver is just made on the copy and not reflected outside.
  • Pointer receiver. User a pointer in order to pass the receiver “as by reference type” and to allow the method to make changes.
func (p *Person) mutateMeForPain() {
}

TODO: I wonder if it is possible to pass a struct by reference and not allow the method to mutate it state. Like a read-only pointer (const * const type of thing).

  • Methods (e.g. value or pointer receivers) allow the method to be called on value types and pointer types. Where as a normal function only allow you pass the exact type.
// Normal function
func area(r rectangle) {  
...
}

// Method
func (r rectangle) area() {  
...
}

r := rectangle{...}
area(r)
r.area()

p := &r
// area(p) <-- won't compile
p.area()
  • Same thing if the receiver is a pointer versus the function that takes a pointer. The function require the exact type to be passed.
  • Can you define methods on non-struct types like int? You can, however the type and the method must be declared in the same package. Which means you can’t directly do it on a type like int … unless you type alias it.
type SuperInt int

func (a SuperInt) add(b SuperInt) SuperInt {  
    return a + b
}

a := 5
a.add(SuperInt(10))

Interfaces

An interface defines a set of method signatures. When a type implements all of the methods, it is said to implement the interface. This is similar to interfaces and protocols in other languages.

type Tokenizer interface {
	SplitIntoTokens() []string
}

type ExtendedString string //<-- need to have an alias so we can add methods to string

func (s ExtendedString) SplitIntoTokens() []string {
...
}

tokens := someString.SplitIntoTokens()
  • Empty interface interface{}. The empty interface has no method requirements and thus all types implement the empty interface also referenced as interface{}
func describe(i interface{}) {  
    fmt.Printf("Type = %T, value = %v\n", i, i)
}

describe("This is a string") // Type = string, value = This is a string
describe(42) // Type = int, value = 42
describe(true) // Type = bool, value = true
  • Use i.(T) to get the underlying type of an interface. For example to write an assert (bad example) function.
func assertInt(i interface{}) {  
	a := i.(int)
	fmt.Println(a)
}

assertInt(42)
assertInt("Bomb!") // panic: interface conversion: interface {} is string, not int

// A better approach would be to use
v, ok := i.(int)
// ok will be true if the underlying type is int
// v will be the zero value of the underlying type
// if ok is false (and thus underlying) type is not int
  • You can switch on the underlying type by using i.(type).
switch i.(type) {
case string:
	...
case int:
	...
}
  • Although Go doesn’t have inheritance, you can still form new “Embedded” interfaces that are composed of other interfaces.
type SalaryCalculator interface {  
    DisplaySalary()
}

type LeaveCalculator interface {  
    CalculateLeavesLeft() int
}

type EmployeeOperations interface {  
    SalaryCalculator
    LeaveCalculator
}

// Thus any type that want to conform to EmployeeOperations must implement both
// SalaryCalculator and LeaveCalculator
  • The zero value of interfaces is nil.

Concurrency with Goroutines

  • Goroutine is a function or method that runs concurrently with other functions or methods.
  • Similar to Swift’s async/await and can be though of as light weight threads with tiny overhead compared to OS/CPU threads.
  • Goroutines are multiplexed onto OS threads.
  • Goroutines communicate via Channels.
  • Creating a Goroutine.
func hello() {  
    fmt.Println("Hello world")
}

go hello()
fmt.Println("This will run concurrently with hello()")
time.Sleep(1 * time.Second)
  • NOTE: The go functionCall will schedule the goroutine to be run and return immediately (ignoring any return value from the function/method).

Channels

  • Think of Channels as a pipe (or producer/consumer).
  • Declared using chan T to declare a channel of type T. Meaning the channel can send and receive type T.
  • Zero value of a channel is nil.
  • Use make(chan T) to create a new channel.
channel1 := make(chan int)
hired := make(chan Person)
  • Sending data on a channel (publishing a value).
channel1 <- value
  • Receiving data from a channel.
value := <- channel
  • Sending or receiving is blocking by default. Meaning that the goroutine that reads from a channel will block until a value is written and also a goroutine that writes to a channel will block until a read is done on the channel. See Buffered channels for how to create a channel that will only block once the buffer is full.
  • Deadlock can happen when a goroutine is waiting for a value to be written or read and there is no goroutine handling it. Go runtime can cause a panic for this.
ch := make(chan int)
ch <- 5
// fatal error: all goroutines are asleep - deadlock!
  • Channels are bidirectional by default. Meaning values can be sent and received on the same channel.
  • To declare a unidirectional channel.
// A channel that can only be used for sending
// Not much use!
ch := make(chan<- int)

// Instead you will use channel conversion
// To "constrain" that the method may only read or write on.
func sendOnly(chan<- int) {
	chan <- 310
}

ch := make(chan int) // can read and write
go sendOnly(ch)
<- ch
  • Use close(channel) to close a channel. Normally senders use this to indicate no more values are coming down the pipe.
  • Receivers can use the pattern v, ok := <-channel to check if ok is false to indicate the channel has been closed.
  • The for range can be used to iterate over values until the channel is closed.
for v := range channel {
...
}