This is a soft introduction to Go for those who have never coded in a typed language before. Go is a simple language and exposes some syntax like pointers which are hidden in other higher level languages. You might have heard of concurrency and channels in passing and you will come across these concepts in Go. There are also some interesting types in Go which can improve code performance if used in right places, i.e. empty interface and struct.
Quick Links:
Pointers
Empty Interface
Empty Struct
Concurrency
Pointers
Variables are passed as arguments into a function by their values in Go, meaning that the variable themselves are not modified by the function. A new copy of the variables are created and stored in separate memory spaces the moment they are passed as arguments. To actually change the variables through the operations in the function, we must pass the variables by pointers by adding a ampersand in front of the variables.
Let's see some examples to illustrate the concept
Pass by values
Run the following code, you'll find the variable remain unchanged
package main
import "fmt"
func changeNumber(number int){
number = 5
}
func main() {
i := 40
changeNumber(i)
fmt.Println(i)
}
Pass by pointers
Notice the two symbols used here:
*
applied on a pointer denotes the underlying value of the pointer, which allows us to change the original value the pointer stored.
*
applied to a Go type, denotes a pointer of that type.
&
applied on a variable generates a pointer of that variable.
package main
import "fmt"
func changeNumber(number *int){ //pass pointer type *int, which is a pointer to the type int
*number = 5 // set number through the pointer
}
func main() {
i := 40
changeNumber(&i) //pointer to variable i
fmt.Println(i)
}
Empty Interface
An object is of a certain interface type if it implements all of the methods detailed in the interface. An empty interface is an interface that specifies zero methods. Therefore any type implements the zero method empty interface.
type empty interface{} // named empty interface
var empty interface{} // anonymous empty interface
This is useful where a function does not know the type of the input parameters, for example:
package main
import "fmt"
func printAnything(something interface{}){
fmt.Printf("%v is of type %T\n", something, something)
}
func main() {
// declare variable "anything" to be an empty interface type
var anything interface{}
// assign 1 to the variable, which is of type int
anything = 1
printAnything(anything)
// reassign the variable with a different type, allowed because "anything" implements the empty interface
anything = "word"
printAnything(anything)
}
Empty Struct
A struct in Go is a way to construct an object, a struct contains named fields each has a name and a type.
type empty struct{} // named empty struct
var empty struct{} // anonymous empty struct
An empty struct is special in rust because it has no memory cost:
fmt.Println(unsafe.Sizeof(empty)) //size 0
This makes an empty struct a perfect option for maps that checks the existence of an element or filter through an duplicated array, or make a synchronisation channel:
package main
import "fmt"
func main() {
// an array of duplicated integers
nums := []int{1, 2, 3, 4, 5, 1, 2, 3}
// map to stored unique integers from the array
checkNumExist := make(map[int]struct{})
for _, num := range nums {
checkNumExist[num] = struct{}{}
}
for key := range checkNumExist {
if _, exist := checkNumExist[key]; exist {
fmt.Printf("num %d exists!", key)
}
}
}
Concurrency
Normally you would expect your code to execute from top to bottom in the exact order you've written it. Concurrency when the code execution is out of order, multiple computations happening at the same time to get a faster result. If the number of tasks is more than the number of processors available, time slicing is a technique used by the processors to simulate concurrency by switching between tasks. A common concurrency related problem is race condition, whereby the relative timing of the concurrent modules can return an inconsistent result if you were running the program non-concurrently. They are hard to debug because the reproducibility of the same outcome is hard.
In terms of communication, there are two models for concurrent programming:
Shared Memory
Concurrent modules have the same access to the same object.
Message Passing
Concurrent modules communicate through channels, sending and receiving messages from and to channels.
Make Channels
In Go, the syntax for making channels:
ch1 := make(chan int) //unbuffered channel, takes one message
ch2 := make(chan int, 5) //buffered channel, takes multiple messages
Sending to an unbuffered channel will be blocked until the message has been received. Sending to a buffered channel on the other hand is a non-blocking operation as long as the channel hasn't used up its full capacity.
Write a message to a channel:
ch1 <- 1
Receive/read a message from a channel:
msg := <- ch1
msg, closed := <- ch1 // closed is true if channel closed
Open a thread
To run a function in a separate thread, add the keyword "go" before you call the function. All goroutines share the same memory space, to avoid race conditions make sure you synchronise memory access e.g. by using "sync" package.
package main
import (
"fmt"
)
func main(){
// make an int type channel
ch := make(chan int)
// anonymous goroutine
go func(){
fmt.Println(<- ch) // print the received
}()
ch <- "Hello World!" //send to channel
}
This is a simple example on a goroutine, pay attention to the order of the goroutine, it comes before the send. This is because for an unbuffered channel, communications are sync, use buffered channels for async.
Here's another example on coordinating goroutines, taken from riptutorial:
func main() {
ch := make(chan struct{})
go func() {
// Wait for main thread's signal to begin step one
<-ch
// Perform work
time.Sleep(1 * time.Second)
// Signal to main thread that step one has completed
ch <- struct{}{}
// Wait for main thread's signal to begin step two
<-ch
// Perform work
time.Sleep(1 * time.Second)
// Signal to main thread that work has completed
ch <- struct{}{}
}()
// Notify goroutine that step one can begin
ch <- struct{}{}
// Wait for notification from goroutine that step one has completed
<-ch
// Perform some work before we notify
// the goroutine that step two can begin
time.Sleep(1 * time.Second)
// Notify goroutine that step two can begin
ch <- struct{}{}
// Wait for notification from goroutine that step two has completed
<-ch
}
Side Notes on Go Map
The Go's implementation of a map is a hashmap. A hashmap uses a hash function that takes a key and returns a deterministic value derived from the key. Each item in the hashmap gets an unique index, so called a hash. The hashmap data structure is an array of buckets, each contains a pointer/unique index to an array of key-value pairs. The map methods are converted to calls to the runtime during code compilation. To read more about Go maps click here.
Useful resources & References
Tour of Go
Net Ninja Youtube playlist
Concurrency - MIT
Concurrency - RIP tutorial
Top comments (0)