Hacker News new | comments | show | ask | jobs | submit login
Common Mistakes for New Golang Devs (cloudimmunity.com)
190 points by BerislavLopac 5 months ago | hide | past | web | favorite | 35 comments



One of the most common mistakes new Go developers make are creating race conditions.

For example, the author's final example. (And maybe others, I didn't read them all.)

  $ go run -race racy.go
  ==================
  WARNING: DATA RACE
  Write at 0x00c42009800f by goroutine 6:
    main.main.func1()
        /home/eric/racy.go:12 +0x38
  
  Previous read at 0x00c42009800f by main goroutine:
    main.main()
        /home/eric/racy.go:15 +0x8b
  
  Goroutine 6 (running) created at:
    main.main()
        /home/eric/racy.go:11 +0x76
  ==================
  done!
  Found 1 data race(s)
  exit status 66
edit: add code listing

  package main
  
  import (  
      "fmt"
      "runtime"
  )

  func main() {  
      done := false
  
      go func(){
          done = true
      }()
  
      for !done {
          runtime.Gosched()
      }
      fmt.Println("done!")
  }


Yes, race conditions are definitely common. Thank you for pointing out the race condition in that example!

That code is intentionally simplified to make a point about the scheduling gotcha :-) The idea there was to show that a tight loop in one goroutine ( where the scheduler doesn't have an opportunity to execute) can prevent other goroutines from executing.


Why does this race?

After setting done = true, is there no guarantee that the goroutine will complete before the for loop ends?


there's no guarantee, they are running potentially concurrently. So the same variable is accessed from two goroutines with no mutex.


The assignment isn't guaranteed to be an atomic operation.


But there are no concurrent writers to the bool, the `true` setting should thus "eventually succeed" and reading the bool must resolve to either false or true at every point. Sure: maybe the first read happening concurrently with the write won't read true yet but surely the next one or the one after must?

Of course synchronization in-general is a must. But using a bool here with one writer and continuous-reading-until is probably the least-dangerous-of-possible-examples.. even a number or anything slicish incl strings combined with an operator other than equals would, in the same code-snippet length, make the dangers illustrable and live-observable when running even without `-race` =)


> Sure: maybe the first read happening concurrently with the write won't read true yet but surely the next one or the one after must?

You are expecting the read operation to be done either before or after the write, which would be the case if atomicity was guaranteed. There might be ugly intermediate states, or maybe there aren't, but don't know because there are no guarantees.

See also: http://preshing.com/20130618/atomic-vs-non-atomic-operations...


> There might be ugly intermediate states

In the general case: absolutely! In the above very specific setup as I outlined it (one bool, a single writer, a simple-waiting-for-that-single-write-loop) --- not. (Hence my point that another equally-tiny snippet might make the issue much more locally-reproducable-and-experienceable even without `-race`.)


What should the code be instead?


There are many possible fixes. The most obvious is to wrap it in a mutex (assuming 'done' is standing in for much more complex logic):

  func main() {  
      doneMutex := sync.Mutex{}
      done := false
  
      go func(){
          doneMutex.Lock()
          defer doneMutex.Unlock()
          done = true
      }()

      getDone := func() bool {
        doneMutex.Lock()
        defer doneMutex.Unlock()
        return done
      }
  
      for !getDone() {
          runtime.Gosched()
      }
      fmt.Println("done!")
  }

Another option would be to use a channel in place of done.

Effectively, the lesson here is that go doesn't have concurrently safe types (e.g. no concurrent map) nor generics to create them, so all concurrent code must be built around the builtin generic concurrent-safe type (channels) or must make careful use of mutexes / the atomic package / etc.

Idiomatic concurrent go either is built around channels or mutexes, and in either case the compiler won't tell you if you messed up, only the runtime race detector and testing will save you.


Version 1.9 introduced the sync.Map https://golang.org/pkg/sync/#Map

Would using a sync.WaitGroup and closure be wrong? Is mutex better for shared memory?


sync.Map is NOT a good solution for all concurrent map use cases. Please use your best judgement for the well documented trade-offs:

>The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.


The fix that's probably closest to the author's intent is to use an atomic. There's no atomic boolean in the language, but if you store 0 or 1 in an int32 it works.


A single-item-buffered chan is probably most common, but a sync.WaitGroup w/ a 1 add can work if you don't need to do other ops while waiting.


"In C++ creating variables using the new operator always means that you have a heap variable."

It's hard to make accurate statements about C++ :-) This is not one. Perhaps it would be more correct to say "In C++ a |new| expression is used to construct an object whose lifetime is not necessarily bound to the scope in which it is created" or something like that. It is definitely not true that an object created with |new| is allocated on a heap. It can be placed on the stack, or any place, or maybe not even allocate.


It can even go away. The compiler can remove the allocation if the pointer doesn't escape and its lifetime is constrained by block scope.

C++ Compilers historically haven't been willing to perform the optimization for various reasons, but things are changing recently.


Could you explain?


One example: You can override operator new for a class to, instead of calling malloc, return from a static memory pool.


placement new lets you choose where to construct an object.

It‘s somewhat arcane, though. Few application programmers even know about it (although their C++ books all dutifully mention it, usually without more than a single sentence).


It's not that arcane, we used it all the time for pooling, particle systems, in-place loading, etc. Super handy but also like most things in C++, dangerous if you don't know what you're doing.


Moreover, the outcome of a new expression isn't strongly defined. Your class may override operator new for any class-type T, and you may also override global operator new, and you may do whatever it is that you like when you do so. Same for delete.


Constructors can fail via exceptions.


I tried to approach Go as a better C and alternative to C++. C++ has too many warts. I was hoping Go fixed them. A lot of them it did, but articles like this reminds me how many warts Go still has.

> Ken Thompson was once asked what he would do differently if he were redesigning the UNIX system. His reply: "I'd spell creat with an e."

Similar naming is littered through Go. It's like he never learned.



And yet we have package names like "fmt".


Ping me if you have other gotchas you'd like to add. There'll be a few more soon (JSON, Cgo, etc).



How so?


> On error, any Response can be ignored. A non-nil Response with a non-nil error only occurs when CheckRedirect fails, and even then the returned Response.Body is already closed.

https://golang.org/pkg/net/http/#Client.Do


The articles states:

>"It's OK to call a pointer receiver method on a value as long as the value is addressable. In other words, you don't need to have a value receiver version of the method in some cases.

Not every variable is addressable though. Map elements are not addressable."

Since everything in memory has an address I am confused by these statements. Does the author mean that "not everything can be pointed to" with a pointer variable in Go?


Go itself says you "cannot take the address" of some values. For example:

  m := map[int]int{1: 2}
  p := &m[1]
The second line causes the compilation error "cannot take the address of m[1]".

https://play.golang.org/p/pIMgeQzOSZK


Thanks, I think it was the wording "addressable" that was the source of my confusion. "cannot take the address of" seems more intuitive to me. Cheers.



It would be great if you could elaborate on this: "Sending to an Unbuffered Channel Returns As Soon As the Target Receiver Is Ready"

As far as I understand, the problem is that the printing goroutine is forced to exit when the main goroutine exits. This way it's unknown if all the submitted work was done.

It seems to me that the caption is a bit misleading, it implies that the problem occurs only with "Unbuffered" channels.


I am not sure why

* Unused Variables

* Unused Imports

are being considered traps or gotchas. They are in fact very sensible design decisions for a compiled language. Having a compile-time error because of mistyping a variable is far better than having to debug a runtime failure.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: