9 ms·
Common Gotchas in Go
- nordsieck 9y agoSyntax critique: https://play.golang.org/p/CjIDCu5ojru https://play.golang.org/p/CjIDCu5ojru
- 0xmohit 9y agoIn 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 9y agoA language with a linter, testing, and everything built in? Sign me up!
- 0xmohit 9y agoYou 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
- sergiotapia 9y agoAlso check out Elixir. Testing as first-class baked in, data randomizer for tests baked in, formatter baked in, BEAM VM, pattern matching, the list goes on and on and on.
- mohaine 9y agoThis is one of Go's best features. Static binaries is the other killer feature.
- dilap 9y agoI 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
- Matthias247 9y agoI agree on all of these. #1 would even count as gotcha when it would the loop would yield a reference instead of the a copy, since lots of code might expect a copy. I think in some other languages (e.g. C#) there was also some confusion and change about that. There are of course also the usual gotchas when capturing things in a closure (is it by value or by reference?). But this is not Go specific. You explained the main Go-specific Gotcha in my opinion: The dual-nullability of interface types, and the fact that non-null interface objects might contain null pointers to structs that implement the interface.
- Dylan16807 9y ago> I think in some other languages (e.g. C#) there was also some confusion and change about that. Nah. They wanted references, they got references. The issues were an oversight related to scope.
- catnaroek 9y ago> 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).
- innagadadavida 9y agoLooks 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 9y agoOr pass it into the func, especially if its running in a goroutine. for _, ele := range eles { go func(ele string) { // use ele }(ele) }
- roskilli 9y agoAlso the other "fun" approach: for _, elem := range elems { elem := elem; go func() { /* use elem */ }() }
- frou_dh 9y agoIt's a shame to have code end up like this when Go is supposed to be anti- "stutter".
- innagadadavida 9y agoThis 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 9y agoNo, 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.
- crypticlizard 9y agoI 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!
- ernsheong 9y agoThe 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 9y ago(&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 9y agoThanks for pointing it out.
- mappu 9y agoMy Go gotcha: x = append(y, ...) is usually unsafe for x != y. I want to find/build a linter that catches such cases...
- Denzel 9y agoCan 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 9y agoThey 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 9y agoExactly. 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 9y agoThanks 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
- yeukhon 9y agoOn #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.
- tonetheman 9y agoYeah 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.
- maddyboo 9y agoTl;dr: Understand the difference between values and pointers.
- pcwalton 9y agoThere are lots of languages in which values and pointers are different that do not have these gotchas.
- dingo_bat 9y agoDidn't read the article yet but congrats on the awesome domain name!
- JepZ 9y agoMy 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.
- maddyboo 9y agoRe: #1 - You can always make your type implement the marshal/unmarshal method(s) so that it knows how to [de]serialize itself.
- dantheman0207 9y agoThe 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?
- derivagral 9y agoNot GP: I'd guess if you briefly forget that pointers are memory values you'll run into some Fun when trying to re-create a struct and use it as a key.
- JepZ 9y agoIn 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).
- jerf 9y ago
- millstone 9y agoHow 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?
- oconnor663 9y agoI make defensive copies when I'm worried about it. Append will reuse the current backing array when it has enough capacity to hold whatever you're appending. So to alias maximally like you suggested, you could try creating all your arrays/slices with lots of extra capacity. That would be pretty expensive, but you could try doing it one-off just to see what happens in your test suite?
- JelteF 9y agoIt definitely can lead to hard to find bugs. I even made an issue out of one case where it could happen in very unexpected ways (which is now fixed on the go master branch): https://github.com/golang/go/issues/21149 https://github.com/golang/go/issues/21149 Apart from the issue above I've found that it's usually quite easy to reason about though. Usually when you take a slice you either use it temporarily without modifying or you throw away the original one. In the rare case where you do still need both slices to outlive the same function you just copy one of them manually defensively.
- ominous_prime 9y agoIt'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.
- drej 9y agoThe 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 9y agothe append way is much efficient than the make+copy way: https://github.com/go101/go-benchmarks/tree/master/append-vs-make https://github.com/go101/go-benchmarks/tree/master/append-vs...
- drej 9y agoNot 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 9y agoI 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
- tapirl 9y agoPlease note that the Benchmark_AllocWithMake function even doesn't call "copy(y, x)".
- dilap 9y agoI am surprised that make would be slower than append! What's the explanation?
- rplnt 9y agoI seriously need a browser extension to not render "characters" in color. How the hell did this happen anyway?
- tapirl 9y agoFor #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 9y agoNot 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 9y ago"[]string(nil)" is more verbose than "nil".
- AnIdiotOnTheNet 9y agoSeriously? That's your problem with this construct that otherwise does exactly what you asked for?
- tapirl 9y agoIt is easy for compilers to deduce the type of the nil in "append(nil, data[:2]...)".
- AnIdiotOnTheNet 9y agoSure, 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.
- nazka 9y agoThere 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-go-golang/ http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in...
- jijji 9y agoit 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...
- moocowtruck 9y agoI 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!