Go gotchas, surprises, puzzles.
Other lists and articles:
- 50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs
- Do you make these Go coding mistakes?
- Common Gotchas in Go
- Dark Corners of Go
- Gotchas in the Go Network Packages Defaults
The JSON unmarshaler works with embedded struct, while literal initialization will not.
prog.go:31:13: cannot use promoted field A.A1 in struct literal of type B
The high index of a slice is not what it seems.
Question: Does the following snippet compile? Run? Panic?
package main
func main() {
v := [6]int{0, 1, 2, 3, 4, 5}
w := v[:]
w = w[:4]
w = w[:0]
// Question: Before you hit run! What do the following two lines result in?
w = w[1:]
w = w[1:3]
// fmt.Printf("%v - len: %d, cap: %d\n", w, len(w), cap(w))
}
Try it out yourself:
panic: runtime error: slice bounds out of range
The zero value for a pointer is nil.
Each element of such a variable or value is set to the zero value for its type: false for booleans, 0 for numeric types, "" for strings, and nil for pointers, functions, interfaces, slices, channels, and maps. -- Zero value
If the return value is pointer to a struct, e.g. *E
and we return nil
, we
do not actually return nil
.
References:
TBC.
// go run main.go
//
// 2019/08/26 17:19:28 (*main.E)(nil): some error message
// exit status 1
//
package main
import (
"log"
)
type E struct{}
func (e *E) Error() string {
return "some error message"
}
func mayFail(f float32) *E {
if f < 0.5 {
return nil
} else {
return &E{}
}
}
func main() {
var err error
err = mayFail(0.4)
if err != nil {
log.Fatalf("%#v: %s", err, err.Error())
}
}
A similar example: play.golang.org/p/ZfY7AN687ah
cannot use promoted field __ in struct literal of type
Example: One GitHub API (v3) wrapper defines different option types. The SearchOptions embed a ListOptions type for pagination.
The following would not work: "cannot use promoted field ..."
opt := &github.SearchOptions{Sort: "stars", PerPage: 10}
Workaround is to create an options value, then assign to the promoted field.
opt := &github.SearchOptions{Sort: "stars"}
opt.PerPage = 10
There is the concept of a star in programming languages and the concept of a programmer who uses them, e.g. ThreeStarProgrammer.
It's easy to get to three stars in Go using arithmetic and indirection.
package main
import (
"fmt"
)
func main() {
var (
a = new(int)
b = &a
)
**b = 3
fmt.Printf("%d", *a***b)
}
Can you see the result? If not, just try it out!
We do not know exactly, but maybe the semantics around the limit are less clear when writing - or there has not been enough demand.
Kubernetes has an ioutils package, which contains a LimitWriter. Minio has a LimitWriter as well.
Illustrating the point above, currently an ErrShortWrite
will be returned, if
the limit is hit (in some previous version it was called ErrMaximumWrite
).
However, we could just stop writing without returning an error, so io.Copy
would work:
// Branch off a limited number of bytes from resp.Body into a buffer.
var (
buf bytes.Buffer
tee = io.TeeReader(resp.Body, LimitWriter(&buf, 512))
)
if _, err := io.Copy(ioutil.Discard, tee); err != nil {
log.Fatal(err)
}
In Go we often defer function calls, e.g. defer f.Close()
after we opened a file or a defer tx.Rollback()
to rollback a failed SQL transaction.
But what if you want to return an error or result from a deferred function call?
Consider this example where we want to add additional context to an error. One might think that we can simply assign to err
since it is in the closure of the deferred function.
func g() error {
err := fmt.Errorf("new error")
defer func() {
err = fmt.Errorf("more context: %w", err)
}()
return err
}
Surprisingly, this will return the unmodified error.
Effective Go#Recover explains a simple panic recovery mechanism that points out what we are doing wrong:
... deferred functions can modify named return values.
After reading the section about Defer statements in Go's language specification it becomes pretty clear why we have to a named result parameter:
... deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller.
Here's a working example (playground):
func f() (err error) {
err = fmt.Errorf("new error")
defer func() {
err = fmt.Errorf("more context: %w", err)
}()
return err
}
The go tools ignore files starting with either a .
or an _
, e.g. _main.go
would not be considered by the Go tools.
Folders called testdata
are also ignored. Here is the statement from Go's documentation:
Directory and file names that begin with "." or "_" are ignored by the go tool, as are directories named "testdata".
Note that embed
, which was included with Go 1.16, also follows the behavior described above when determining which files to include.
If a pattern names a directory, all files in the subtree rooted at that directory are embedded (recursively), except that files with names beginning with ‘.’ or ‘_’ are excluded.
Let's say you implement a file server and thus need to map a request URL path to local path, e.g. https://<some-domain>/some/file
should serve /<some-base-directory>/some/file
. Without cleaning up the request path you will be open to path-traversal attacks. That means if someone requests https://<some-domain>/../../../etc/passwd
your server will happily respond with passwd
(if it has permission to read it). To prevent this problem you can just do a filepath.Clean(req.URL.Path)
, but, this will not work if req.URL.Path
is not absolute. So, when using filepath.Clean
make sure that the path you're passing into is absolute, when in doubt just add a /
in front.
It was surprising to me that http.Client
and http.DefaultClient
are following up to 10 redirects by default.
I discovered this when doing a HEAD
request on a resource that responds with a redirect, giving a 302 status code,
and a Location
header with the response. But, with the DefaultClient
you will receive a 200
instead, and no Location
header of course.
You can override the default behavior by setting a custom CheckRedirect
function in the client like this:
httpCl := http.DefaultClient
httpCl.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
}
Note that ErrUseLastResponse
will just signal the client to stop following any more redirects.
This one is subtle and caused many servers to crash. One thing in advance, using a recovery middleware in your web server won't safe you!
The following function will not recover and cause your program to crash:
func wontRecover() {
defer func() {
if err := recover(); err != nil {
log.Println("recovered from panic:", err)
}
}()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(1 * time.Second)
panic("panic'ed in goroutine")
}()
fmt.Println("waiting for goroutine to finish")
wg.Wait()
}
Imagine wontRecover
to be an http.Handler
and you will see how widespread this problem is.
The workaround is to recover()
in any subroutine that might panic.
Of the approximately 1400 sentences in ref/spec about 9 mention "fallthrough" - and one says:
The "fallthrough" statement is not permitted in a type switch.
Which got a few thousand people surprised.
Consider the following function that reuses a base URL and creates instances of it, each with a different path.
base := &url.URL{Scheme: "https", Host: "go.dev"}
paths := []string{"/play", "/doc", "/blog"}
urls := make([]*url.URL, len(paths))
for idx, path := range paths {
u := base
u.Path = path
urls[idx] = u
}
for _, u := range urls {
fmt.Println(u)
}
What do you think the function will print? It is not the following, which I had expected:
https://go.dev/play
https://go.dev/doc
https://go.dev/blog
In the loop u := base
will not be a copy of the URL, since base
is a pointer to url.URL
. Instead, u
will just point to the same memory location as base
. There are two simple options for copying a *url.URL
:
- create a new instance by parsing the stringified base URL
u, _ := url.Parse(base.String())
- populate all fields manually:
u := &url.URL{Scheme: base.Scheme, Host: base.Host, Path: path}
Does the following work? Play.
package main
import (
"io"
"os"
)
func main() {
var s io.Reader = os.Stdin
var v any
switch v = s.(type) {
case os.Stdin:
// ...
case *os.File:
// ...
default:
// ...
}
}
Almost: the value of v
only materializes in the switch - there is no point in
fixing the type (even any
) before the switch - in fact just using v = s.(...
would be a syntax error.
./prog.go:13:11: syntax error: cannot use assignment (v) = (s.(type)) as value