Go Brain Teasers
Foreword by David Cheney
As a fan of trivia, in the salad days of my education as a software engineer, one of my favourite books was Josh Bloch and Neal Gafter’s Java Puzzlers. I liked that the authors didn’t simply set out to stump readers with the obscure language factoids. Instead, each question was treated as an opportunity to educate the reader on the history and deeper meaning of a less travelled aspect of the language.
When I discovered, far too many years into my Go experience than I
care to mention, that copy
returned the number of elements copied,
my first thought was wow, how had I missed that. My second thought
was I wonder how many people I can trick with this. Thus was born my
#golang pop quiz series of tweets.
With tweet sizes being what they are the requirement to fit an entire Go program into a single tweet proved a challenge. Divining the correct answer to a #golang pop quiz and the opportunity to follow Bloch and Gafter’s example to educate, rather than frustrate, was left as an exercise to the reader.
In Go Brain Teasers, Miki has prepared a set of tests that seek not to simply baffle, but to enlighten. Go Brain Teasers' provides the reader with the opportunity to learn the why behind that what!?!.
David Cheney
Sydney, April 2020
The Brain Teasers
Eduction is not the learning of facts, but the training of the mind to think.
1. Just in Time
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"encoding/json"
"fmt"
"log"
"time"
)
func main() {
t1 := time.Now()
data, err := json.Marshal(t1)
if err != nil {
log.Fatal(err)
}
var t2 time.Time
if err := json.Unmarshal(data, &t2); err != nil {
log.Fatal(err)
}
fmt.Println(t1 == t2)
}
Try to guess what the output is before moving to the next page. |
This code will print: |
You might expect this code to fail since there’s no time
type in the
JSON format, or you might expect the comparison to
succeed.
Go’s encoding/json lets you define
custom JSON serialization for types that are not supported by JSON.
You do that by implementing
json.Marshaler and
json.Unmarshaler
interfaces. Go’s time.Time
implements these interfaces by marshaling itself
to an RFC 3339 formatted string and
back.
JSON has a very limited set of types. For example it only has floating-point numbers. Which can lead to surprising results if you use the empty interface when unmarshaling.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
n1 := 1
data, err := json.Marshal(n1)
if err != nil {
log.Fatal(err)
}
var n2 interface{}
if err := json.Unmarshal(data, &n2); err != nil {
log.Fatal(err)
}
fmt.Printf("n1 is %T, n2 is %T\n", n1, n2)
}
The above will print n1 is int, n2 is float64
Once you passed JSON serialization, you’d expect the times to be equal. The time package documentation says (my emphasis):
Operating systems provide both a "wall clock," which is subject to changes for clock synchronization, and a "monotonic clock," which is not. The general rule is that the wall clock is for telling time and the monotonic clock is for measuring time. Rather than split the API, in this package the Time returned by time.Now contains both a wall clock reading and a monotonic clock reading;
Monotonic clocks are used for measuring durations. They exist to avoid problems such as your computer switching to daylight saving time during measurement. The value of a monotonic clock by itself does not mean anything, only the difference between two monotonic clock readings is useful.
When you use ==
to compare time.Time
, Go will compare the time.Time
struct fields, including the monotonic reading. However when Go serializes a
time.Time
to JSON, it doesn’t include the monotonic clock in the output. When
we read back the time to t2
, it doesn’t contain the monotonic reading and the
comparison fails.
The solution to the problem is written in time.Time’s documentation
In general, prefer t.Equal(u) to t == u, since t.Equal uses the most accurate comparison available and correctly handles the case when only one of its arguments has a monotonic clock reading.
2. A Funky Number?
1
2
3
4
5
6
7
8
9
package main
import (
"fmt"
)
func main() {
fmt.Println(0x1p-2)
}
Try to guess what the output is before moving to the next page. |
This code will print: |
Go has several number types, the two main ones are:
- Integers
-
These are whole numbers. Go has
int8
,int16
,int32
,int64
andint
.[1]. There are also all the unsigned onesuint8
… - Floats
-
These are real numbers. Go has
float32
andfloat64
.
When you write a number literal, such as 3.14
, the Go compiler needs to
parse it to a specific type (float64
in this case). The
Go spec defines how you can
write numbers. Let’s have a look at some examples:
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
package main
import (
"fmt"
)
func main() {
// Integer
printNum(10) // 10 of type int
printNum(010) // 8 of type int
printNum(0x10) // 16 of type int
printNum(0b10) // 2 of type int
printNum(1_000) // 1000 of type int (1)
// Float
printNum(3.14) // 3.14 of type float64
printNum(.2) // 0.2 of type float64
printNum(1e3) // 1000 of type float64 (2)
printNum(0x1p-2) // 0.25 of type float64 (3)
// Complex
printNum(1i) // (0+1i) of type complex128
printNum(3 + 7i) // (3+7i) of type complex128
printNum(1 + 0i) // (1+0i) of type complex128
}
func printNum(n interface{}) {
fmt.Printf("%v of type %T\n", n, n)
}
1 | _ serves as the thousands separator. It makes big numbers much more
readable for us humans. |
2 | This is known as scientific notation |
3 | The current brain teaser |
0x1p-2
is called "a hexadecimal floating-point literal" in the Go
specification and is following the
IEEE 754 2008 specification. To
calculate the value:
2.1. Further Reading
-
IEEE 754 specification