Out of curiosity, what are the first two?
Out of curiosity, what are the first two?
For me it's:
1. context.Context and putting everything under the sun there, untyped (which is a design fault, not just "people using it wrong")
2. reusing the looping variable in for loop
2.5. general awkwardness of go templating (I plan to write whole article about this, but too lazy to actually write it)
The language makes a big deal about memory management and data structure ownership, and as a result it’s sorta a pain to send stuff around on the stack, both at initialization and runtime. As a casual golang user, the context mess feels like an escape hatch to help with those problems. I’d prefer some GC compromises built into the language itself instead.
FWIW, I think request-scoped globals are terrible and is among the many things I hated about Flask. I think generics will largely solve this problem by allowing us to write generic middleware with a type-safe context and probably even functions to compose the middleware and automatically propagate the context variables.
My concern with its use in golang is that so much seems to get jammed into it. My experience with it is that it ends up being a holding pen for all the things that other systems do with initialization-time dependency injection / resource lookup sorts of things, but replaced with "oh just pass `ctx` as an argument everywhere".
From what I've seen of Go, it looks like that happens because of the state management idiomatic constraints of the language, which in turn seem GC-related.
In a garbage-collected language, I'd put a bunch of context-y things into some sort of request executor datastructure, the web framework would manage the initialization and retention of that, and request handlers would operate against those intialized data structures. That doesn't line up all that well with idiomatic Go, and so we end up needing to hide those sorts of things somewhere else. Enter context-abuse.
Of course, there are plenty of use cases where those sorts of patterns aren't relevant. Right tool for the job and whatnot.
Again, I'm a casual golang user at best -- I bet I'm missing some newer / better approaches, and would love to learn more about what they are.
What (I think) you are suggesting implies globally scoped, mutable variables that affect control flow dramatically. Sounds really hard to reason about and very munch anti-Go-explicitness. Context is the handle by which an outer context holds on to the nested ones.
The only non-bonkers way to have a handle like that would be to give every function a getContext function to query some kind of context tree and signal parent/children. This is totally against Go explicitness as well.
Maybe something like Scala's implicits to sugar it, but that's another not-go-looking pattern (although it would be nice)
Go authors themselves admit it might have been a design mistake, but is unfixable without breaking compatibility.
https://golang.org/doc/faq#closures_and_goroutines
> This behavior of the language, not defining a new variable for each iteration, may have been a mistake in retrospect. It may be addressed in a later version but, for compatibility, cannot change in Go version 1.
How does that work? The call-frame needs to be of fixed-size, but it's rarely knowable at compile time how many iterations a loop will perform. Unless we begin heap-allocating loop variables or treat each loop iteration as a function call (both of which will kill performance, I think), I don't see how this could work?
for _, v := range values {
v := v // create a new 'v'.
go func() {
fmt.Println(v)
done <- true
}()
}
The scope of the variable has no effect on how many values may or may not be allocated.Probably, hence my "How does that work?" question.
I don't doubt your example--I know it works, but I'm struggling to understand how it manages to work, since (as I understand it) the closure presumably captures the environment, which presumably means taking the address to the new `v` which presumably is the same address for each loop iteration?
Whether a loop is involved is irrelevant, it comes up any time a value referenced a closure might outlast the environment which created the closure. That can happen in loops but also if conditions or normal function calls. The value might be promoted to the heap, but in trivial cases like the above it could be copied directly on the new goroutine's stack. Or if escape analysis says the closure can't outlast the environment, it will use the same stack.
for _, v := range values {
go func(v string) {
fmt.Println(v)
done <- true
}(v)
}
No, the 16 bytes will be copied onto the goroutine's stack.Do you think it's smart enough to do the trivial transformation of the first into the second?
Right, I understand the difference between a variable and a region of memory.
> If `values` is an array of strings, do you think the following makes a new string on the heap (that is, a new 16 byte heap allocation, internally pointing to the same variable-sized region of memory)?
No.
> Do you think it's smart enough to do the trivial transformation of the first into the second?
I guess I'm surprised that the semantics for the `v := v` in a loop thing are "this will get allocated somewhere else such that it's guaranteed not to be stomped on by another loop iteration". For example:
for _, v := range []int{2, 4, 6} {
v := v
mut := sync.Mutex{}
go func() {
mut.Lock()
fmt.Println(v)
mut.Unlock()
done <- true
}()
go func() {
mut.Lock()
v = 1
mut.Unlock()
done <- true
}()
}
If it allocates `v` on the goroutines' stacks, then it won't catch the mutation, so presumably it allocates on the heap at least in that case. In whatever case, the semantics are surprising to me.It makes me want to port it to Rust, since so many of the major Rust templating libraries are Jinja copies or Handlebars copies.
1. Error handling. Not only is `if err == nil` (or my preferred `if _, err := foo(); err == nil`) a lot of noise, errors are completely opaque and might as well be strings.
2. Dependency management. It was a mess for a long time, go modules fixed a lot of issues in it's own way but still has problems (e.g. the "v2" workaround). My theory is they expected everyone, in some way, to "self-host" their modules (like how Java would have com.google.foo.bar, eventually everyone else would have "http://go.company/package", but GitHub became central repository of software for everyone and linking directly to packages was no longer sound).
Errors in modern Go are definitely not just strings.
Before wrapped errors in Go, this would have been an easier case to make against the language.
foo := &FooError{}
if errors.As(err, &foo) {
fmt.Println("foo error!")
}Maybe it’s a good thing it’s not widely known, generics are trivial to discover.
It's inability to deal with cyclic references, the lack of overloading, the lack of package-level visibility and the lack of static functions all contribute to a lot of ceremony.
You end up with functions like newUserWithPassword and NewRole (casing intentional), instead of User::new() and Role::new()
You could just have a NewUser that takes a NewUserOpts which exposes a fluent interface. But again, a lot of ceremony.
You can use a package per type, but still no overloading, and you'll need a 3rd package to bridge the two if the reference each other.
After 4 years of Go, this is usually a design thing. Once I figured out how to separate and isolate domains of interest better, I stopped running into these issues entirely. My code, in and outside of Go, is much better for it. I also learned that much of "thread-safe" programming is taught this way, which makes sense.
I've been getting into Go for the past couple of months, but I still struggle with this.
func processItem(item *string) {
if item == nil {
fmt.Println("nil item")
return
}
fmt.Println(*item)
}
stuffToProcess := []string{"one", "two"}
wg := sync.WaitGroup{}
for _, item := range stuffToProcess {
wg.Add(1)
go func(s *string) {
time.Sleep(1 * time.Millisecond)
processItem(s)
wg.Done()
}(&item)
}
wg.Wait()
// What do I print?
Playground here: https://play.golang.org/p/mouvT1BkpNJI assume GGP's contrived example is to show the underlying mechanics of the issue.
[reuse.go:26] - G601 (CWE-118): Implicit memory aliasing in for loop. (Confidence: MEDIUM, Severity: MEDIUM)
25: wg.Done()
> 26: }(&item)
27: }
[1] https://github.com/securego/gosec