Relearning Go: Day 2
Values & Variables to Slices
Day 1 was short because I spent most of it writing an introduction. Today we get serious, beginning with:
Values & Variables
gobyexample.com/values
gobyexample.com/variables
Variables declared without a corresponding initialization are zero-valued. For example, the zero value for an
int
is0
.
From my previous experience of Go, and my experience of programming in general, I think zero values were a bad choice. But whatever, let’s go with it.
Constants
Constant expressions perform arithmetic with arbitrary precision.
If it means what I think it means, this is kind of cool 😎
For
Learning for
, the syntax for range iteration is not consistent either with the
:=
syntax introduced earlier nor with other for
loop syntax:
for i := range 3 {
fmt.Println("range", i)
}
Why not have an in
operator, say? e.g. for i in range 3 { …
.
Anyway, it’s fine, but what’s the mechanism, the principle? What does a range n
look like? Trying fmt.Println(range 3)
yields “syntax error: unexpected
range, expected expression”. Maybe I’m not meant to know? Moving on.
If/Else
A statement can precede conditionals; any variables declared in this statement are available in the current and all subsequent branches.
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
It’s nice to scope num
to the conditional block, but I wonder if this is a
foot-gun in practice? I wonder if I can embed another statement in the else if
clauses?
There is no ternary if in Go, so you’ll need to use a full if statement even for basic conditions.
I’m guessing that if
blocks are not expressions? Checked: no. Curious if
there’s another form for conditional expressions, or do I always need five
lines?
if increment {
foo += 1
} else {
foo -= 1
}
Switch
Maybe this is an expression? Doesn’t look like it, but type switch looks interesting. I seem to remember that it’s relevant later on.
Arrays
Nothing surprising.
Slices
This, however, kicks off WTF season.
An uninitialized slice equals to
nil
and has length 0.
An initialised slice can also have length 0 – e.g. make([]string, 3)
– and
not be equal to nil. Is this distinction useful? Sounds like a confusing rough
edge.
We need to accept a return value from
append
as we may get a new slice value.
We may? I kind of remember this now. This looks like a foot-gun or, worse, a subtle source of bugs. For example, consider:
s := make([]int, 0, 3)
s = append(s, 1)
t := append(s, 2)
s = append(s, 3)
fmt.Println(s)
fmt.Println(t)
This prints:
[1 3]
[1 3]
After a while this makes sense. Or, I have concocted a mental model that can make sense of it, but I don’t yet know if it’s the correct mental model. Either way, this is non-obvious. Go is a language for inexperienced developers, right?
I assume it’s okay in practice, i.e. you learn it and get used to it, but at this point it looks like a missed opportunity to create a clean and unambiguous API that cannot be misused, or that people will accidentally use correctly. This has neither property, and yet it’s a core language feature.
⚠️ Warning: The Go Playground does not save your work. If you accidentally hit the back button, say, like I just did, you’ll lose your experiments. The TypeScript Playground and Rust Playground, in contrast, do save.
Slices have inconsistent syntax too:
t := []string{"g", "h", "i"}
fmt.Println(t) // prints: [g h i]
So square brackets are for arrays and slices, but curly braces and commas to populate, but back to square brackets and no commas for display? Why?
Lastly for today, the slices example suggests reading Go Slices: usage and internals. It’s a good read, and confirms that mental model of slices I had come up with earlier, but it makes me think of a couple more weirdnesses.
First, echoing back to earlier: an uninitialised slice is nil
, but nil
is
not an uninitialised slice.
var s []int
fmt.Println(s == nil) // prints: true
s = append(s, 1) // no problem
t := append(nil, 1) // does not compile
Second, another demonstration of the confusing behaviour of append
:
s := make([]int, 0, 1)
s = append(s, 1)
t := append(s, 2)
s = append(s, 3)
s[0] = 4
fmt.Println("s =", s) // prints: [4 3]
fmt.Println("t =", t) // prints: [1 2]
It makes sense, but feels like append
should be a low-level or internal
function, not a core building block.
That’s all for slices, and enough for day 2.