August 13th, 2024 08:10 UTC · 2 months ago

Golang

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 is 0.

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

gobyexample.com/constants

Constant expressions perform arithmetic with arbitrary precision.

If it means what I think it means, this is kind of cool 😎

For

gobyexample.com/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

gobyexample.com/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

gobyexample.com/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

gobyexample.com/arrays

Nothing surprising.

Slices

gobyexample.com/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.