DevilKing's blog

冷灯看剑,剑上几分功名?炉香无需计苍生,纵一穿烟逝,万丈云埋,孤阳还照古陵

0%

Go the good, the bad and the ugly

原文链接

Good

new Types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type UserId string // <-- new type
type ProductId string

func AddProduct(userId UserId, productId ProductId) {}

func main() {
userId := UserId("some-user-id")
productId := ProductId("some-product-id")

// Right order: all fine
AddProduct(userId, productId)

// Wrong order: would compile with raw strings
AddProduct(productId, userId)
// Compilation errors:
// cannot use productId (type ProductId) as type UserId in argument to AddProduct
// cannot use userId (type UserId) as type ProductId in argument to AddProduct
}

Bad

Go ignored advances in modern language design

functional programing

No LLVM

GC

Interfaces are structural types

Go interfaces are like Java interfaces or Scala & Rust traits: they define behaviour that is later implemented by a type (I won’t call it “class” here).

No enumerations

iota

The:=/var dilemma

Go provides two ways to declare a variable and assign it a value: var x = "foo" and x := "foo".

The main differences are that var allows declaration without initialization (and you then have to declare the type), like in var x string, whereas := requires assignment and allows a mix of existing and new variables. My guess is that := was invented to make error handling a bit less painful:

Zero values that panic

go have no constructor

1
2
3
4
5
6
7
8
9
var m1 = map[string]string{} // empty map
var m0 map[string]string // zero map (nil)

println(len(m1)) // outputs '0'
println(len(m0)) // outputs '0'
println(m1["foo"]) // outputs ''
println(m0["foo"]) // outputs ''
m1["foo"] = "bar" // ok
m0["foo"] = "bar" // panics!

上面所示,m1初始化了,但m0没有初始化。。

Go doesn’t have exceptions.

just “Defer, panic and recover

Ugly

The dependency management nightmare

Things are getting better though: dep, the official dependency management tool was recently introduced to support vendoring.

But dep may not live long though as vgo, also from Google, wants to bring versioning in the language itself and has been making some waves lately.

mutability is hardcoded in the language

Go makes it easy however to copy an entire struct with a simple assignment, so we may think that passing arguments by value is all that is needed to have immutability at the cost of copying.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type S struct {
A string
B []string
}

func main() {
x := S{"x-A", []string{"x-B"}}
y := x // copy the struct
y.A = "y-A"
y.B[0] = "y-B"

fmt.Println(x, y)
// Outputs "{x-A [y-B]} {y-A [y-B]}" -- x was modified!
}

And the since built-in collections (map, slice and array) are references and are mutable, copying a struct that contains one of these just copies the pointer to the same underlying memory.

Slice gotchas

Slices come with many gotchas: as explained in “Go slices: usage and internals“, re-slicing a slice doesn’t copy the underlying array for performance reasons.

This is a laudable goal but means that sub-slices of a slice are just views that follow the mutations of the original slice. So don’t forget to copy() a slice if you want to separate it from its origin.

Forgetting to copy() becomes more dangerous with the append function: appending values to a slice resizes the underlying array if it doesn’t have enough capacity to hold the new values. This means that the result of append may or may not point to the original array depending on its initial capacity. This can cause hard to find non deterministic bugs. (在append的时候,如果没有足够的空间,会重新resize slice,这样pointer会乱指)

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
func doStuff(value []string) {
fmt.Printf("value=%v\n", value)

value2 := value[:]
value2 = append(value2, "b")
fmt.Printf("value=%v, value2=%v\n", value, value2)

value2[0] = "z"
fmt.Printf("value=%v, value2=%v\n", value, value2)
}

func main() {
slice1 := []string{"a"} // length 1, capacity 1

doStuff(slice1)
// Output:
// value=[a] -- ok
// value=[a], value2=[a b] -- ok: value unchanged, value2 updated
// value=[a], value2=[z b] -- ok: value unchanged, value2 updated

slice10 := make([]string, 1, 10) // length 1, capacity 10
slice10[0] = "a"

doStuff(slice10)
// Output:
// value=[a] -- ok
// value=[a], value2=[a b] -- ok: value unchanged, value2 updated
// value=[z], value2=[z b] -- WTF?!? value changed???
}

mutability and channels: race conditions made easy

As we saw above there is no way in Go to have immutable data structures. This means that once we send a pointer on a channel, it’s game over: we share mutable data between concurrent processes. Of course a channel of structures (and not pointers) copies the values sent on the channel, but as we saw above, this doesn’t deep-copy references, including slices and maps, which are intrinsically mutable. Same goes with struct fields of an interface type: they are pointers, and any mutation method defined by the interface is an open door to race conditions.

noisy error management

In “Error has values“ Rob Pike suggests some strategies to reduce error handling verbosity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type errWriter struct {
w io.Writer
err error
}

func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return // Write nothing if we already errored-out
}
_, ew.err = ew.w.Write(buf)
}

func doIt(fd io.Writer) {
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
return ew.err
}
}

nil interface values

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Explodes interface {
Bang()
Boom()
}

// Type Bomb implements Explodes
type Bomb struct {}
func (*Bomb) Bang() {}
func (Bomb) Boom() {}

func main() {
var bomb *Bomb = nil
var explodes Explodes = bomb
println(bomb, explodes) // '0x0 (0x10a7060,0x0)'
if explodes != nil {
println("Not nil!") // 'Not nil!' What are we doing here?!?!
explodes.Bang() // works fine
explodes.Boom() // panic: value method main.Bomb.Boom called using nil *Bomb pointer
} else {
println("nil!") // why don't we end up here?
}
}

struct field tags: runtime DSL in a string

Go has few data structures beyond slice and map