Hacker News new | ask | show | jobs
by erik_seaberg 3143 days ago
That's easy. Concurrent access to maps is unsafe. https://golang.org/pkg/sync/#Map should be a drop-in replacement but due to the lack of generics it can't present a compatible or typesafe API.
1 comments

I've run into concurrency problems using maps while writing an irc client. It was really frustrating for me, because it did not happen everytime I ran the program, but rather rarely. I had chosen Go for it's memory-safety and easy concurrency. But I felt like I could not vouch for my program's safety any more. I did not know about sync/map at the time, had I known, I would probably have used it.

I think Golang has a fondness for magical (to me) special language constructs. For example if you want to use the RPC modules, you have to write your functions a certain way. Or as I've explained in a comment above, if you want to use an interface you just have to implement all it's properties. Or you can't have generics, but we have a special language construct called maps, where you kind of can mimick genericks but it is not safe and may break.

Golang is a great language, but I think it expects all its users to think very rigidly in the same way their designers do. Basically, you have to explore the language's deficiencies yourself, read the designers' explanations, dive into the intrinsics of the language, understand it, make yourself believe and move on. I sometimes half-jokingly feel like Golang follows the principle of most obedience.

> I did not know about sync/map at the time

Apparently also not about sync.Mutex =)

> if you want to use an interface you just have to implement all it's properties

You surely mean "methods" --- don't interfaces work like that everywhere they exist?

I discovered Waitgroups during the project, but I have no formal training in concurrent programming, so it was a bit daunting for me. And as the error occured very rarely I could not be sure if using a mutex had solved my problem. But now I understand mutex was the way to go for accessing a shared resource.

As for the second point, sorry for the wrong wording, I meant "methods". But interfaces don't work like that everywhere they exist. For example in Java you explicitly use Class X implements Y, and if you do not implement all methods and properties, the compiler complains.

> I discovered Waitgroups during the project

Don't know about your exact map use-case you had at hand, but generally speaking the Mutexes suffice for Lock()ing/Unlock()ing a resource that is accessed concurrently, whereas the WaitGroups serve usually best to just fire off a couple of parallel jobs (that don't access any shared-between-them resources) and then simply Wait() for them all to finish. No real protection from concurrent accesses in there, other than for the WaitGroup's own hidden internal counter that's used to Wait()

Of course these are the low-level sync primitives, channels are the higher-level abstraction, but the former can take you far --- sometimes the latter are the sanest/only choice of course. Especially for parallel goroutines needing to work with each other's intermediate results.

Java interfaces don't have properties either (nor do Java classes), and you can explicitly state interface conformance in Go as well as others have explained elsewhere in this debate: https://news.ycombinator.com/item?id=15672619

What Java does have (since 8) is default method implementations in interfaces.

Sorry, my fault. It seems in java you can declare constants in interfaces, but this is frowned upon.

  Interfaces cannot require instance variables to be defined -- only methods.
  (Variables can be defined in interfaces, but they do not behave as might be expected: they are treated as final static.)
(source: https://stackoverflow.com/questions/7311274/attributes-membe...)

(Another useful link explaining that statements resembling instance variables in interfaces are in fact constants: https://coderanch.com/t/178630/certification/Instance-variab...)

For concurrency issues, -race flag is your friend.

Don't blame the language for your buggy code.

Unless otherwise stated, Go's data structures are not thread-safe. Maps are not thread safe.

In that respect Go isn't different from any other mainstream language with pre-emptive threading (C++, Java, C#).

go, a language which comes with many built in concurrency primitives, has chosen to leave its core datatypes thread unsafe (the magic ones with generic powers) and gives no sensible tools to remedy this with. this is one of the many ways in which it is a language hostile to its users.
Often, you don’t need concurrent types and their overhead. The sync package makes it easy to bolt on if you truly do.

It’s not as if every block is sending out each line to run in a separate goroutine. Go encourages users to share data by communicating, not communicating by sharing data [0].

On phone, sorry for syntax

    struct CMap {
        sync.RWMutex
        i int 
    }

    func (m *CMap) Inc() {
        m.RWLock()
        defer m.Unlock()
        m.i++
    }
[0]: https://blog.golang.org/share-memory-by-communicating
i understand the patterns and design intentions of the language, despite not having written very much of it. (i just checked my work stats and i'm surprised to find that i've written/changed nearly 40k lines of go - of course that counts for less given how verbose the language is). one of my complaints is that a language designed to be simple has made (to me) incomprehensible design choices in the name of efficiency.

for instance, in a for loop ranging over a slice:

  for _, i := range some_slice {
    go func() {
      something(i)
    }()
  }
i is re-used for every iteration of the loop, so the asynchronous function does not close over i as a particular value, but rather i as whatever value the loop has iterated to by the time it is referenced. i see experienced go programmers make this mistake sometimes, even when they've run into it before. my take is that the golang designers chose to implement this behaviour to either simplify their work or in the name of efficiency. if it's the former, it's indefensible. if it's the latter, why not offer user-friendly semantics (let closing over i as in the example capture the iterated value) and simplify the generated code at compile time when possible?

for your example, you can look to the newly added sync/map which uses interface{} everywhere as an example of how the lack of generics have left users with no _sensible_ tools to remedy these kinds of language problems. how wonderful would an efficient thread-safe and type-safe concurrent map be? it's possible in many other modern and not so modern programming languages, but it is intentionally not possible in go.

Java gets this right.

  for (int i : someCollection) {
      pool.execute(() -> something(i));
  }
Because "i" is effectively final, each Runnable closes over the correct value for that iteration. If "i" were being reassigned, the closure wouldn't be allowed to use it, you'd have to decide whether to close over a final copy or a reference to a mutable object that holds the latest value over time (a one-element array is the idiom).
>Don't blame the language for your buggy code.

No, DO blame the language if its the one that enables and allows for said buggy code when it could as well prevent it.

Case in point: http://lambda-the-ultimate.org/node/3186

Fair enough, in that case.

However a common situation is that the language is supporting a wide range of uses, not just the use a particular developer has in mind at the time.

I've seen this most commonly where a developer operating at a higher level of abstraction, without understanding low level details, complains that the language doesn't match his high level needs exactly.

When in fact it supports lower level operations for maximum performance or flexibility, while allowing layering on top for higher level uses. In language design it's essential to support the lowest level use cases you are targeting, because you can build on top but not go lower than the language exposes (unless it supports embedded asm or another trick to go lower again). I'm using higher/lower here only to refer in the sense of the level of abstraction.

It's a really common trap for someone working usually at one level of abstraction and not deeply understand other levels, to not understanding why a language or library is the way it is because if needs to support others users needs that are different to ones own - I've done it myself enough to detect the pattern, and seen lots of other people do it. Lurking on design committee discussions can be eye opening!

But I can't implement a drop in compatible concurrency safe map.
Don't blame the language for that: jump through BS hoops to create one such map /s