
Common Gotchas in Go - omn1
https://deadbeef.me/2018/01/go-gotchas
======
dilap
I think only #1 really counts as a gotchya. #3 is literally just "spend at
least 2 seconds knowing what a slice is" (which you should do! there's only
like 4 data structures in Go built in, so get to know 'em all). #2 is similar.

My favorite gotchya is that assigning a nil pointer to an interface will not
give you a nil interface.

Once you know what's going on, it totally makes sense, but usually you don't
start out with that deep an understanding of how interfaces work, and it
violates natural-looking assumptions about algebraic equality, which makes for
very easy counter-intuitive behavior.

Example:

    
    
        package main
    
        import "fmt"
    
        func main() {
            var x *ErrorX = nil
            var err error = x
            if err == nil {
                fmt.Println("nope, this won't happen ")
            } else {
                fmt.Println("here's my non-nil error:", x)
            }
        }
    
        type ErrorX struct{}
    
        func (x *ErrorX) Error() string {
            return "hi i'm ErrorX"
        }
    

[https://play.golang.org/p/BB1b2_HDb6I](https://play.golang.org/p/BB1b2_HDb6I)

~~~
catnaroek
> Once you know what's going on, it totally makes sense

Anything “makes sense” from an operational point of view, so long as you
internalize the rules. But does Go's handling of `nil` denote anything you
ever actually _want_ to express, regardless of language?

> it violates natural-looking assumptions about algebraic equality

No, Go's handling of `nil` doesn't violate anything about equality: everything
(besides floats, but you can't fault Go for those) is equal to itself and to
nothing else. But the _syntax_ (i.e. using a single unadorned token `nil`)
does a very poor job of reflecting the language's _semantics_ (the existence
fo multiple `nil` values, all different from one another).

~~~
dilap
I'm sure there are uses for it; in this case, since making nil pointers _not_
work would require extra rules, it doesn't seem worth doing. A suggestion I
saw on reddit was to use a different word for "nil" for interfaces ("unset" or
something), which would make the gotchya have less bite.

And I wasn't trying to say nil-to-interface violated anything real, just that
it violated natural-to-make-but-actually-untrue _assumptions_ (which is I
guess pretty much the definition of a gotchya):

If we have

    
    
        a = nil
        b = a
    

it's natural (but incorrect) to assume that we have

    
    
        b == nil
    

Basically, I agree with you that the syntax is being poorly suggestive of the
semantics. I like the "unset" suggestion because then we'd have:

    
    
        a = nil
        b = a
    

which wouldn't falsely suggest

    
    
        b == unset
    

Too late now, though.

------
millstone
How do Go programmers deal with the fact that it is hard to reason about
whether slices alias?

Go lacks const, so there's no way to prevent your caller from modifying the
slice you return, or the callee from modifying the slice you pass it. Also the
result of append() may or may not alias its parameter, depending on the
allocator's mood that day.

Do Go APIs just defensively copy, or pepper APIs with "do not modify"
warnings, or rely on runtime tests? Given the fact that append() may mask
aliasing bugs, is there a way to make append() alias maximally?

~~~
ominous_prime
It's rarely a concern in practice. Public APIs should document if a slice may
or may not be modified, for example: `io.Writer` says ` Write must not modify
the slice data, even temporarily`, and `bufio.Scanner.Bytes()` says `The
underlying array may point to data that will be overwritten by a subsequent
call to Scan`.

The `append` builtin may allocate a new array and copy, or reuse the same
array, but the result should almost always be assigned back to the original
variable. The only time you wouldn't assign it back to the same variable is
when you are carefully managing the slices yourself for some specific reason,
which should never be exposed via an API.

~~~
catnaroek
Do you also rely on function documentation telling you whether it modifies its
arguments, “even temporarily”? Or do you just read the source for every
function you use, transitively?

------
yeukhon
On #3 “slicing”, on a related note for Python, remember after a new list is
created from slicing, each element/item in the new list is the exact reference
to the corresponding element/item in the original list. This is espeically
problematic when you try to do things on a mutuable object in either list, but
expect the other list remains completely shield from change. You have to do
deepcopy (but for immutable object deepcopy has no effect).

Makes sense and probably is a given for most programming languages.

------
JepZ
My newest two gotchas:

1\. Don't use private attributes for struct types if you ever want to
serialize/encode them (exception: Mutex and Channel Attributes). Just had to
refactor a whole lot of code just, because I decided I wanted to save the
struct to disc. Btw. the type itself can and probably should be private.

2\. Don't use Pointers in map key structs. As nesting maps is a pain the
simple solution is to use structs as keys. But if you do so please remember
not to use any pointer fields within those structs. Again when you encode and
decode maps with pointers within the key structs those pointers will bite you.

~~~
dantheman0207
The private attributes thing has also but me in the ass...encode/decide
functions won’t touch private variables and won’t throw an error if all
variables in a struct are private. What kind of issues did you run into with
pointers inside of a struct in a map?

~~~
JepZ
In my case it wasn't just that I used pointers which might work in some cases.
Instead I used a pointer to a global variable containing a function reference
(directly using a function reference was not possible as those are not
'compareable'). Therefore, whenever I started the programm the global variable
had a different location and therefore it didn't macht the location it had
when the file was saved (during an earlier execution).

------
maddyboo
Tl;dr: Understand the difference between values and pointers.

~~~
pcwalton
There are lots of languages in which values and pointers are different that do
not have these gotchas.

------
crypticlizard
I liked how this was written. The way this was written sort of reminds me of
go. It's as simple as approriate, and written in a way to be memorable. Nice
UX trick deadbeef!

------
nordsieck
Syntax critique:
[https://play.golang.org/p/CjIDCu5ojru](https://play.golang.org/p/CjIDCu5ojru)

~~~
0xmohit
In fact, `gofmt` can help simply this. `gomft -s` would simply

    
    
        for idx, _ := range zoo {
            zoo[idx].legs = 999
        }
    

to

    
    
        for idx := range zoo {
            zoo[idx].legs = 999
        }

~~~
chisleu
A language with a linter, testing, and everything built in? Sign me up!

~~~
0xmohit
You may want also to watch a talk by Alan Donovan, co-author of The Go
Programming Language [0], on static analysis tools [1].

[0] [http://www.gopl.io/](http://www.gopl.io/)

[1] [https://talks.golang.org/2014/static-
analysis.slide#1](https://talks.golang.org/2014/static-analysis.slide#1)

------
tapirl
For #2, the "if len(a) == 0" clause is totally non-sense.

I hope Go supports "append(nil, data[:2]...)", which will make code much
cleaner.

~~~
smnscu
Not sure what you mean but it seems to work for me

[https://play.golang.org/p/U9Uuy-_sdjn](https://play.golang.org/p/U9Uuy-_sdjn)

~~~
tapirl
"[]string(nil)" is more verbose than "nil".

~~~
AnIdiotOnTheNet
Seriously? That's your problem with this construct that otherwise does exactly
what you asked for?

~~~
tapirl
It is easy for compilers to deduce the type of the nil in "append(nil,
data[:2]...)".

~~~
AnIdiotOnTheNet
Sure, and it might just in a future version of the spec if there isn't a good
reasoning for not doing that.

Doesn't mean that "it's more characters!" isn't still a really silly
complaint.

------
drej
The last gotcha sort of leads to a different gotcha. That copy statement will
only work as expected if the destination slice is large enough - thus always
initialise it with the length of the (sub)slice you want to copy over. You
won't get any errors if it's smaller, but you will surely not get what you
desired.

~~~
tapirl
the append way is much efficient than the make+copy way:
[https://github.com/go101/go-benchmarks/tree/master/append-
vs...](https://github.com/go101/go-benchmarks/tree/master/append-vs-make)

~~~
drej
Not sure it holds any more, I got these results

    
    
      $ go test -bench=.
      goos: darwin
      goarch: amd64
      Benchmark_AllocWithMake-4     	    1000	   1626664 ns/op
      Benchmark_AllocWithAppend-4   	    1000	   1574720 ns/op

~~~
seppoastian
I got nearly the same ratio as the github example. (CPU is overclock to ~4
GHz)

    
    
      $ cat /proc/cpuinfo | grep 'model name' | uniq
      model name: Intel(R) Core(TM) i7-5820K CPU @ 3.30GHz
      
      $ go version
      go version go1.9.1 windows/amd64
      
      $ go test -bench . --benchtime 10s
      goos: windows
      goarch: amd64
      Benchmark_AllocWithMake-12    20000  870349 ns/op
      Benchmark_AllocWithAppend-12  30000  561120 ns/op

------
mappu
My Go gotcha: x = append(y, ...) is usually unsafe for x != y.

I want to find/build a linter that catches such cases...

~~~
Denzel
Can you explain this in more detail?

I've never used this construct, but I was unaware of its consequences. You've
piqued my interest.

~~~
Everlag
They may be referring to the idea that the result of that operation is defined
based on the y's capacity, cap(y). (Capacity is the total amount of space in
the slice backing y whereas length is the number of entries used in y; append
will not allocate until the new length > capacity)

If the capacity is less than the new length, a new backing array will be be
allocated. This results in x != y

However, if the capacity is sufficient to contain the new values then x == y.

Normal usage of append is overwriting the initial slice variable so you don't
need to worry about this, x = append(x, ...). If you use two variables, as
here, you can potentially have two different slices used as though they were
equivalent in later code.

~~~
mappu
Exactly. The inadvertent aliasing can produce situations like this:
[https://play.golang.org/p/XUtyQ6ShYaz](https://play.golang.org/p/XUtyQ6ShYaz)
, or more plausibly:
[https://play.golang.org/p/Xx8lZWVWgIu](https://play.golang.org/p/Xx8lZWVWgIu)

~~~
innagadadavida
Thanks for this. It gets weirder! The slice grows/doubles if capacity is
insufficient. So if you initialize the original slice all the way to capacity
(len=4, cap=4), it works correctly. But if len=3 and cap=4, it shows bad
behavior.
[https://play.golang.org/p/qf3lAoin4dM](https://play.golang.org/p/qf3lAoin4dM)

------
nazka
There was a good post about many gotchas that was on HN a little while ago.
Just to share :

[http://devs.cloudimmunity.com/gotchas-and-common-mistakes-
in...](http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-
golang/)

------
jijji
it also takes some figuring that the first character of anything needs to be
uppercase if 1) you try to a function outside of the main source file, and 2)
use an element from a struct in a template, i.e. {{.element}} does not work...

------
ernsheong
The first one could be mitigated by providing an array of article pointers
([]*Article). That way, article.legs = 99 would work.

Else, the way mentioned in the article, or (&article).legs = 99 (ugly) would
work. Take your pick.

~~~
roskilli

      (&article).legs = 99
    

This would not work actually because it would only be modifying the copy that
exists in the inner loop of the for loop. You'd be taking a pointer to the for
loop stack variable and modifying it.

If they were already references as you first mention that way simply
article.legs = 99 would work yes.

~~~
ernsheong
Thanks for pointing it out.

------
innagadadavida
Looks like a newbie article. The gotcha that got had me a few times is closure
within a loop:

for _, ele := range eles { go func() { // use ele } }

Since ele is a loop variable it is not captured as you might think it is. You
need to copy it before the closure.

~~~
anonfunction
Or pass it into the func, especially if its running in a goroutine.

    
    
        for _, ele := range eles { go func(ele string) { // use ele }(ele) }

~~~
frou_dh
It's a shame to have code end up like this when Go is supposed to be anti-
"stutter".

~~~
innagadadavida
This might have been done by Go for perf reasons. Why should the Go compiler
copy a new variable on the stack every loop iteration?

~~~
Merovius
No, not really, it's a natural and unintended consequence of how the spec
scopes variables in loops/switches/conditionals:

[https://golang.org/ref/spec#Blocks](https://golang.org/ref/spec#Blocks)

The problem you are trying to solve is, that with a statement like

    
    
        for i := 0; i < n; i++ {
            doAThing(i)
        }
    

you want `i` to be valid inside the loop body, in the for-clauses but _not_
outside the for-statement. Go solves this by saying that an if/for/switch
statement has an implicit block surrounding them and that block is what scopes
the loop variables. AFAIK no one considered, that this would have this
consequence in relation to closures.

Performance wouldn't really matter, because compilers tend to be pretty good
at optimizing these kinds of things. They already reorder when they check for
the condition and how they jump non-intuitively and the naive instruction
sequence would involve freeing some stack-space at the end of the loop,
reallocating it in the next iteration and then writing the new value to it.
Figuring out that you can save the actual stack-pointer operations isn't that
hard.

~~~
innagadadavida
Can the compiler implicitly copy i, basically convert it to:

    
    
      for i := 0; i < n; i++ {
        i := i
        doAThing(i)
      }

~~~
Merovius
Sure. It's an easy problem to fix, now that the team is aware it. I'm pretty
sure they'll solve it for Go 2. In the meantime, it would break the
compatibility promise, so it's just a kludge we have to live with.

I basically just wanted to point out that the problem isn't so much an
implementation question (or about performance). But that no one thought of it
and it's now mainly a question of how to best phrase the spec for this :)

------
tonetheman
Yeah the range thing got me too.

At some point I tend to just ignore the second part of the range and only use
the index...

and then after that I just switch back to a damn for loop like god intended.

------
dingo_bat
Didn't read the article yet but congrats on the awesome domain name!

------
rplnt
I seriously need a browser extension to not render "characters" in color. How
the hell did this happen anyway?

------
moocowtruck
I look at stuff like this and wonder why I'm reading it because I'll never use
this language.. but i'm glad it's here for people who do!

