Go Programming Tips

Add 0x prefix when printing hex numbers

Use %#x placeholder.

1
2
3
4
5
func TestPrintHex()  {
var i int = 0x123456
fmt.Printf("%x\n", i)
fmt.Printf("%#x\n", i)
}
1
2
123456
0x123456
1
2
3
fmt.Printf("%T\n", i)
fmt.Println(reflect.Typeof(i))
fmt.Prinft("%s\n", reflect.Typeof(i).String())

Get random numbers

Intn() returns the next integer in the current random sequence. UnixNano() returns the number of nanoseconds elapsed since January 1, 1970 UTC. And the result of UnixNano() does not depend on the location associated with t.

We only have to set the seed once. But before we set the seed, rand will use the default seed. For example, the first call of Intn(100) always returns 81 without setting the seed.

1
2
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Intn(100))

Temporary variables in if-else block

In Go, we can define variables in if-else condition statement. The scope of these temporary variables is limited within if-else block.

1
2
3
4
5
if v, p := math.Sqrt(float64(sum)), math.Sin(float64(sum)); v < 10 && p < 0.5 {
fmt.Println("True")
} else {
fmt.Println(v, p)
}

switch

In Go, the execution does not fall through the switch structure. Switch cases evaluate cases from top to bottom, stopping when a case succeeds. Also, if we want to take the same action on multiple cases, we can write case content1, content2:.

In the following example, although without any break, the control flow jumps out of the switch block after executing the first case.

1
2
3
4
5
6
7
8
9
var i int = 15640
switch {
case i > 1:
fmt.Println(i)
case i > 2:
fmt.Println(i)
case i > 99999:
fmt.Println("false")
}
1
15640

Slice

Slices share the same underlying memory with the original array.

  • The type of slice is []T.
  • len() returns the number of elements a slice contains.
  • cap() returns the number of elements in the underlying array, counting from the first element in the slice.
  • append(slice, e1, e2, ...) overwrites the elements in the underlying array if slice has enough capacity to contain these elements. Otherwise, it returns a new allocated slice and keeps the underlying array intact.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func TestSlice() {
var arr [5]int = [5]int{0, 1, 2, 3, 4}
slice1 := arr[0:4]
slice2 := arr[1:4]
fmt.Println(arr)
fmt.Println(slice1)
fmt.Println(slice2)
fmt.Printf("type: %s, len: %d, cap: %d\n", reflect.TypeOf(arr).String(), len(arr), cap(arr))
fmt.Printf("type: %s, len: %d, cap: %d\n", reflect.TypeOf(slice1).String(), len(slice1), cap(slice1))
fmt.Printf("type: %s, len: %d, cap: %d\n", reflect.TypeOf(slice2).String(), len(slice2), cap(slice2))
slice1[0] = 9
fmt.Println(arr)
fmt.Println(slice1)
fmt.Println(slice2, "\n")

// Test slice append
slice3 := append(slice2, 8) // change the underlying array
slice4 := append(slice2, 7, 6) // return a new allocated slice
fmt.Println(arr, &arr[0], cap(arr))
fmt.Println(slice1, &slice1[0], cap(slice1))
fmt.Println(slice2, &slice2[0], cap(slice2))
fmt.Println(slice3, &slice3[0], cap(slice3))
fmt.Println(slice4, &slice4[0], cap(slice4))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[0 1 2 3 4]
[0 1 2 3]
[1 2 3]
type: [5]int, len: 5, cap: 5
type: []int, len: 4, cap: 5
type: []int, len: 3, cap: 4
[9 1 2 3 4]
[9 1 2 3]
[1 2 3]

[9 1 2 3 8] 0xc000018270 5
[9 1 2 3] 0xc000018270 5
[1 2 3] 0xc000018278 4
[1 2 3 8] 0xc000018278 4
[1 2 3 7 6] 0xc00001e080 8

Variable Length Array

Go only allows constants to be used as array size. To create an array whose size is defined by a variabl, we can use make([]T, length). It creates an underlying array with size of length and returns a slice of it.

1
2
3
4
5
6
7
8
9
10
11
12
13
const conLen = 8

func TestVLA(length int) {
var l int = 5
// illegal
//var arr0 [len]int
//var arr1 [l]int

var arr2 = make([]int, l)
var arr3 = make([]int, length)
var arr4 [conLen]int
// do something
}

String comparison

In Go, we can compare strings by using comparison operators (==, <, >, etc) or by strings.Compare(s1, s2).

File naming convention

snake_case is the convention across the most of the standard library and most third party libraries for Go.

defer

defer FunctionCall() defers the execution of FunctionCall() until the surrounding function (the parent function call of FunctionCall()) returns. Multiple deferred calls are stacked, so that they will be callled in a LIFO order.

1
2
3
4
5
6
func TestDefer()  {
for i:= 0; i < 5; i++ {
defer fmt.Println("deferred call", i)
}
fmt.Println("ready to return")
}
1
2
3
4
5
6
ready to return
deferred call 4
deferred call 3
deferred call 2
deferred call 1
deferred call 0

Hashmap

In Go, we can declaring a hashmap by using key word map. map declares a non-thread-safe hashmap.

  • Create a hash map: hashmap := make(map[string]int), string is key type and int is value type.
  • Insert a key-value pair: hashmap[key] = value.
  • Delete a key-value pair: delete(hashmap, key).
  • Check whether a key exists: v, ok := hashmap[key]. If the key exists, ok == true. Otherwise, v equals to the zero value of the value type, ok == false.
  • Traverse the hashmap: for k, v := range hashmap {// do someting}

Difference between make and new

  • make returns a value of T; new returns a value of *T.
  • make returns an initialized value; new returns a zeroed value.
  • make can only create and initialize slices, maps and channels; new can allocate zeroed memory for any types.

Take hashmap as an example. Since we need to initialize structure of hashmap itself before using it (it means that we cannot just zero the hashmap memory), we have to create a hashmap by hashmap := make(map[string]int) or hashmap := map[string]int{}.

Format go files recursively

1
go fmt path/...

Method

In Go, methods take either a value or a pointer as the receiver when they are called. Go interprets s.Print() as (&s).Print(), ps.SwapPrint() as (*ps).SwapPrint(). But this is just syntactic sugar.

Also, we cannot define new methods for existing types in another package (including built-in types, such as int, map). But it is fine to define a normal function that takes a non-local-package type argument. Or we can define our own alias for that type in current package type Alias ExistingTypeName, or define a wrapper structure containing that type type NewStruct struct {n ExistingTypeName}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type MyStruct struct {
x int
y int
}

func (ps *MyStruct) Print() {
fmt.Printf("(%d, %d)\n", ps.x, ps.y)
}

func (s MyStruct) SwapPrint() {
fmt.Printf("(%d, %d)\n", s.y, s.x)
}

func TestMethod() {
ps := new(MyStruct)
ps.x = 12
ps.y = 24
s := MyStruct{12, 24}
ps.Print()
ps.SwapPrint()
s.Print()
s.SwapPrint()
}

Interface

Any concrete types that implement all methods of a interface type satisfy implicitly that interface type. In other words, a interface type specifies a set of methods that a concrete type must possess to be considered an instance of that interface.

Note that T and *T are different when it comes to the relationship between interfaces and concrete types. And we cannot define methods for types under another package.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type IntTask int
type FloatTask float64

type Runner interface {
Run()
}

func (pit *IntTask) Run() {
fmt.Printf("running task id = %d\n", int(*pit))
}

func TestInterface() {
var task Runner
var pit *IntTask = new(IntTask)
*pit = 1
var it IntTask = 2

fmt.Println(int(*pit))
fmt.Println(int(it))

task = pit
// illegal
//task = id
task.Run()
}

Since only *ID implements Run() method, *ID satisfies interface Runnable but ID does not.

1
2
3
1
2
running task id = 1

Empty interface

interface{} is empty interface. Because it has no methods, all types satisfy the empty interface.

Interface Pitfall

An interface value with a nil dynamic value is NOT the same as a nil interface for any non-empty interfaces.

If we assign nil to an empty interface variable, that variable is still equal to nil.

If you want to keep the interface value consistent with its concrete value, please do not define a concrete value first then assign it to the interface value, but define the interface value directly (allocate a concrete value by new).

1
2
3
4
5
6
7
8
9
10
11
func TestPitfall() {
var pit *IntTask // zeroed as nil
var run1 Runner = pit // warning: run1 is not nil
var run2 Runner // zeroed as nil
var run3 Runner = new(IntTask)
var emptyInterface interface{} = nil
fmt.Printf("run1 == nil ? %t\n", run1 == nil)
fmt.Printf("run2 == nil ? %t\n", run2 == nil)
fmt.Printf("run3 == nil ? %t\n", run3 == nil)
fmt.Printf("emptyInterface == nil ? %t\n", emptyInterface == nil)
}
1
2
3
4
run1 == nil ? false
run2 == nil ? true
run3 == nil ? false
emptyInterface == nil ? true

Sort

sort.Ints() and sort.Strings() only accept slice as argument. sort.Sort() provide a customizable method that requires the data type to be sorted should implement Len(), Swap() and Less().

1
2
var arr = [10]int{2, 3, 4, 6, 7, 9, 2, 3, 4, 5}
sort.Ints(arr[:])

Type Assertion

The general form of type assertion is x.(T), while x is an interface to be tested (asserted), T can be a concrete type or another interface type. There are two usages of type assertion.

  1. T is a concrete type. The type assertion checks whether the concrete type of x is identical to T.
  2. T is an interface type. The type assertion checks whether the concrete type of x satisfies interface T.
  3. A successful type assertion returns the dynamic value in type T, and an optional assertion flag. A failed type assertion returns nil and flag false, if the statement only accepts one return value, it will cause a panic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func TestTypeAssert() {
var itRunner Runner = new(IntTask)
var ftRunner Runner = new(FloatTask)

itRunner.Run()
ftRunner.Run()
// if without ok flag, such as pit2 := ftRunner.(*IntTask)
// false assertions will cause panic
pit, okInt := itRunner.(*IntTask)
pit2, okInt2 := ftRunner.(*IntTask)
pft, okFloat := ftRunner.(*FloatTask)
pft2, okFloat2 := itRunner.(*FloatTask)
fmt.Printf("assert itRunner to *IntTask: %t %T %p\n", okInt, pit, pit)
fmt.Printf("assert ftRunner to *IntTask: %t %T %p\n", okInt2, pit2, pit2)
fmt.Printf("assert ftRunner to *FloatTask: %t %T %p\n", okFloat, pft, pft)
fmt.Printf("assert itRunner to *FloatTask: %t %T %p\n", okFloat2, pft2, pft2)
*pit = 15640
*pft = 15.64
itRunner.Run()
ftRunner.Run()
}

Type Switch

We can use a type switch statement to replace an if-else chain of type assertions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
func TestTypeSwitch() {
var i int = 15640
var j float64 = 15.64
var k string = "DS"
TypeSwitch1(i)
TypeSwitch1(j)
TypeSwitch1(k)
fmt.Println("")
TypeSwitch2(i)
TypeSwitch2(j)
TypeSwitch2(k)
fmt.Println("")
TypeSwitch3(i)
TypeSwitch3(j)
TypeSwitch3(k)
fmt.Println("")
}

func TypeSwitch1(x interface{}) {
switch x := x.(type) {
case int:
fmt.Printf("case int: value = %d, type = %T\n", x, x)
case float64:
fmt.Printf("case float64: value = %f, type = %T\n", x, x)
default:
fmt.Printf("Unexpected Type\n")
}
}

func TypeSwitch2(x interface{}) {
switch x.(type) {
case int:
fmt.Printf("case int: value = %d, type = %T\n", x, x)
case float64:
fmt.Printf("case float64: value = %f, type = %T\n", x, x)
default:
fmt.Printf("Unexpected Type\n")
}
}

func TypeSwitch3(x interface{}) {
switch x := x.(type) {
case int, float64:
fmt.Printf("Valid Type: %T\n", x)
default:
fmt.Printf("Unexpected Type\n")
}
}
1
2
3
4
5
6
7
8
9
10
11
case int: value = 15640, type = int
case float64: value = 15.640000, type = float64
Unexpected Type

case int: value = 15640, type = int
case float64: value = 15.640000, type = float64
Unexpected Type

Valid Type: int
Valid Type: float64
Unexpected Type

map, channel and reference in Go

First of all, there is NO strict reference (alias) in Go, which means two variables refer (not point) to the same memeory location. In Go, everything is passed by value.

But we can see that in Go map and channel can be passed by reference in function. This is because map and channel are pointers naturally, while do not look like pointers. For example, a map variable is actually a pointer to a runtime.hmap structure.

By the way, channels act as FIFO queues.

Closed and drained channel

Read requests (not matter how many times) to a closed and drained channel always immediately returns a zero value. But we can examine receives on a closed and drained channel by using optional flag.

1
2
3
4
5
6
7
8
9
10
11
func TestChannel() {
channel := make(chan int, 10)
close(channel)
for {
x, ok := <- channel
if !ok {
break
}
fmt.Println(x)
}
}

Or we can read elements from a channel by range, which will terminate the loop after reading the last element in a closed channel.

1
2
3
4
5
6
7
8
9
10
func TestChannel() {
channel := make(chan int, 10)
channel <- 1
channel <- 2
channel <- 3
close(channel)
for x := range channel {
fmt.Println(x)
}
}

Unidirectional channel types

chan<- T: send-only channel; <-chan T: receive-only channel. They can be used in function arguments to restrict the opertions to channels.

Conversions from bidirectional channel to unidirectional channel types are permitted in any assignment. But there is no going back.

nil channel

Send and receive operations on a nil channel block forever.

select

In select, each case specifies a communication (a send or receive operation on some channel) and an associated block of statements.

  • If there is only one runnable case, execute that case.
  • If there is multiple runnable cases, randomly execute one of them.
  • If there is no runnable cases, block.

The default case in a select is run if no other case is ready. We can use select and a closed channel to terminate goroutines politely.

Terminate a goroutine

First, there is no way for one goroutine to terminate another directly. If we want to terminate a goroutine, we should send a “signal” to that goroutine and let it handles the signal by executing return. This signal can be implemented by a closed and drained channel. Because each receive operations to a closed and drained channel always immediately returns a zero value.

It sounds great, right? But the problem is that if a goroutine is blocked, it cannot notice the signal at the same time. Thus, we need to ensure two things:

  1. If this routine may be blocked by other methods (listener.Accept(), etc), unblock these methods in Close(), for example, by listener.Close().
  2. If this routine may be blocked by channel operations, always wrap every channel operation with a select-done structure. In other words, every channel operation should have a done case in parallel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var done = make(chan bool)        // always empty
var workChan = make(chan int, 10) // a buffered channel storing data

func Close() {
// broadcast termination signal
close(done)
}

func sampleRoutine() {
// ...
select {
case workChan <- data:
// do some non-blocking tasks
case <-done:
return
}
// ...
}

func sampleRoutine2() {
// ...
select {
default:
// do some non-blocking tasks
case <-done:
return
}
// ...
}

If we use for range to receive data from a channel in a goroutine function, which makes it hard to receive the termination signal, it can be rewritten by for{} and select.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func sampleForRange() {
// terminate loop automatically when workChan is closed and drained
// cannot receive
for res := range workChan {
// ...
}
}

func sampleForSelect() {
for {
select {
case res, ok := <-workChan:
if !ok { // workChan is closed and drained
return
}
// ...
case <-done:
return
}
}
}

An ingenious example of decoupling on channel operation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// work routine pulls integers from input channel
// squares them and pushes results to output channel
func (sq *SquarerImpl) work() {
var toPush int
dummy := make(chan int)
pushOn := dummy
pullOn := sq.input
for {
select {
case unsquared := <-pullOn:
toPush = unsquared * unsquared
pushOn = sq.output
pullOn = nil
case pushOn <- toPush:
pushOn = dummy
pullOn = sq.input
case <-sq.close:
sq.closed <- true
return
}
}
}

The constant generator iota

In a const declaration, the value of iota begins at zero and increments by one for each item in the sequence.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const (
zero = iota // 0
one // 1
two // 2
three // 3
four // 4
)

const (
first = 1 + iota // 1
second // 2
third // 3
)

const (
_ = 1 << (10 * iota)
KB // 1024
MB // 1024 * 1024
GB // 1024 * 1024 * 1024
)

Asynchronous Request Implementation

1
2
3
4
5
type Request struct {
reqType ReqType
retChan chan interface{}
reqBody []interface{}
}

Remove the first element of a slice

It also applies on slices with length of 1.

1
slice = slice[1:]

json.Marshal

json.Marshal(v) takes a memory object (argument type interface{}) and return its json encoding. In network programming, it can conveniently convert a user-defined object into json payload of the packet.

Flexible array members in a structure

Go allows us to define flexible array members (slices) in structures. Different from C, we can define any number of slices at any places in a structure.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Struct1 struct {
i int
body []interface{}
}

type Struct2 struct {
body []interface{}
}

type Struct3 struct {
i int
body1 []interface{}
body2 []interface{}
j int
}

Pop from a slice

s = s[1:]. It removes the first element of s. Note that when using this syntax, the length of s can be 1 or greater but cannot be 0.

1
2
3
4
5
6
7
8
func TestSlicePop() {
s := make([]int, 1)
s[0] = 16540
s = s[1:] // remove the first element
fmt.Println(len(s)) // print 0
s1 := make([]int, 0)
s1 = s1[1:] // ERROR!
}

Delete from a map

Deleting a non-existing key from a map through delete() makes no effect.

1
2
3
4
5
6
7
8
func TestMapDelete() {
hashmap := make(map[int]int)
hashmap[1] = 1
delete(hashmap, 0)
for k, v := range hashmap {
fmt.Printf("[%d, %d]\n", k, v)
}
}
1
[1, 1]

Constants in Go

Go does NOT support constant members in structures or constant structures.

Implement timing events by ticker

We can create a ticker through time.NewTicker(interval), which generates ticks in its channel ticker.C at specified time intervals (obviously, the size of channel C is one. We do not want ticks to be accumulated). We can place the handling functions of timing events and other events in a for-select block, so that timing events will not affect the execution of normal events.

Note that Stop() will stop sending ticks to the channel C but will not close it, to prevent a closed drained channel issue.

1
2
3
4
5
6
7
8
9
10
11
12
func TestTicker()  {
ticker := time.NewTicker(10 * time.Second)
//ticker.Stop()
for {
select {
case t := <-ticker.C:
// timing event
fmt.Println(t)
// case e := <-normalEventChan:
}
}
}

Implicit type conversions and implicit numeric conversions

Go does not allow implicit type conversions, and does not allow implicit numeric conversions except numeric conversions across constants.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func TestType() {
var i int = 10
var j int64 = 10
var k int = 10
// 1/10 == 0 => 0.0
var p float64 = 1 / 10
// 1.0/10 => 1.0/10.0 => 0.1
var q float64 = 1.0 / 10
// illegal
// var m float64 = 1.0 / i
_ = j // to keep compiler happy

// mismatched types
// j = i
// mismatched types
/*
if i == j {
fmt.Println("i == j")
}
*/
if i == k {
fmt.Println("i == k")
}
fmt.Println(p)
fmt.Println(q)
// Go does not support implicit numeric conversion
// p = i / k
}
1
2
3
i == k
0
0.1
Author

Weihao Ye

Posted on

2021-12-22

Updated on

2021-12-22

Licensed under