25. Memory Mischief
func dummyMessage() string {
return strings.Repeat("hello", 1000)
}
func main() {
messages := []string{}
for idx := 0; idx < 10; idx++ {
message := dummyMessage()
messages = append(messages, message[:5])
}
runtime.GC()
fmt.Println(messages)
}
How many bytes are still allocated at HERE
by main()
?
- 50184 Bytes
- 234 Bytes
- 734 Bytes
Playground Link
Let's explore some dark corners of our favorite language (or language that will become the favorite).
Don't worry, it will be challenging at times and I don't think you can answer all of them without looking things up - at least I couldn't.
The questions will also get harder one by one. Many questions are not entirely go specific and other languages probably act weirdly too here.
We'll do this as a proper quiz, so in the end there will be a winner!
Take a guess how often you will be right!
The answer is surprisingly three (i.e. 2).
Everyone who guessed it right gets one point. Oh wow.
Trick question.
The range syntax is valid since Go 1.22, but I did not use `i` so it fails to compile in any case.
Answer 3. Strings store bytes and len reports the number of bytes.
But when iterating over a string it iterates over runes, i.e. c is of type rune.
Each rune is a unicode codepoint.
Empty string (2). The `xo` set just trims every x or o character it finds.
It often gets confused with TrimSuffix.
Answer 2: When uint32 overflows it starts at 0 again.
For int32 it's harder to visualize, but imagine it as (-2**31 + 2**31)
Answer 3: Dividing through 0 only panics for integers.
For floats it yields +Inf (For -1/0 it would be -Inf)
Different to python by the way!
It's random. Maps do not guarantee a valid iteration order.
You would need to use a btree if you need that.
Answer 1.
Deletion during iteration is safe in Go. That's because delete() does not free space up immediately
but rather sets a flag that this values can be cleaned up later.
In contrast to deletion, insertion is not safe during iteration.
The number of loops therefore vary between 3 and 6.
</div>
Just the middle one works. In an any map, the type is important and part of the value.
false - a variable of type any (or some other interface type) is like a Pointer
to another variables. Since that pointer is not nil by itself it prints false.
</div>
Answer 2. The trick is just variable shadowing. x (but not y) is re-defined in the if body.
It's number 1. The compiler can't decided which method to call. (`ambiguous selector c.M`)
Number one again. A nil slice is slightly different than an empty slice.
Always check with len() to see if it's empty. A nil slice can be useful because it does not allocate any memory.
Sometimes annoying with json, where it prints null nstead of [].
Answer 5
Slices share the same underlying memory. Therefore the change to s2 will also show to the other ones.
The syntax with the two colons is the cap syntax. It can be used to re-cap a slice.
iota works only inside const blocks. It always starts with zero. When a constant does not have an explicit calculation attached to it,
then the previous one is continued (as for B). It starts with zero for each const block anew.
Before 1.22 the loop variable was captured.
Now, a new loop variable is created, which it works as expected.
It's complicated and each language has their own definition.
Please read it up here: https://torstencurdt.com/tech/posts/modulo-of-negative-numbers/
For Go, it's Answer 2.
The f(1) is called immediately. Otherwise defer calls are stacked. They are executed in reverse order then - last in, first out.
So Answer 3.
Answer 3 (3 0)
The method B.M() has a value receiver. Therefore the change is not carried out and gets lost after the execution is done.
A.M() has a pointer receiver which is automatically picked even though the value we call it on is not a pointer. Here the values survives therefore.
Answer 3.
select{} simply blocks forever without busy polling. Since it's a single go routine we panic because an deadlock is detected.
A closed channel always returns the zero value in a select (and also when doing `v, ok := <-ch // when ok=false`).
Therefore the program loops forever. Classic mistake that can really grind the cpu.
We put 10 items into `ch` (a buffered channel with 5 items) in a separate go routine.
In the main routine we pull 5 items out of it and then close the channel.
Since we might close the channel before the write is finished, we might panic.
But not always since this is a race condition. Therefore answer 2.
A label can be assigned to a for, select or switch so that we either continue with that loop or break from it.
This can be useful to break out of nested loops. Continuing in nested loops is seldomly used and a bit weird.
Here we skip the first inner loop run if y == 0, thus only the second is printed (y = 1). For the outer loops
we break out of it when x = 1 - that's the termination condition anyways so nothing changes and we print twice.
If you listened to my performance talk,
you might know :-)
A slice has 24 Bytes overhead + contents.
A string has 16 Bytes overhead + contents.
One string consists of 1000 5-character words (hello).
Each of those strings have ()(1000 * 5) + 16) bytes.
10 of those strings are created, and even though they are sub-sliced
we do store the full array behind that string, so it's 10 times.
Since we store it in slice we have 24 bytes more.
((1000 * 5) + 16) * 10 + 24 = 50184