This has absolutely been my experience as well. Golang is good at being fast, but to say that it helps write better code because of its missing batteries/features is just silly I think.
"I want to interact with a REST API and pull a field out of its response JSON" is an incredibly common workflow, and yet to do that in golang is far from trivial. You need to define serializer types and all sorts of stuff (or you can take a route I've seen encouraged where people to use empty interfaces, which can cause runtime exceptions).
Same deal with a worker pool. Concurrency is great, but instead of providing a robust, well written solution as part of the language itself, it gives you a toy like this https://gobyexample.com/worker-pools (still the most common result on Google) that is only 80% of the way there. Then you find yourself bolting things onto it to cover your features (we need to know if things fail, so let's just add another channel. We also need finality, whelp, another channel it is), and before you know if you have an incomprehensible mess.
> "I want to interact with a REST API and pull a field out of its response JSON" is an incredibly common workflow, and yet to do that in golang is far from trivial
// Interact with a REST API and pull a field out of its response JSON.
func interact(url string) (field string, err error) {
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("error making HTTP request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("error querying API: %d %s", resp.StatusCode, resp.Status)
}
var response struct {
Field string `json:"the_field"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return "", fmt.Errorf("error parsing API response: %w", err)
}
return response.Field, nil
}
IMO this is a good level of abstraction for a language's standard library. Each of the concrete steps required for the process are simply and adequately represented. Each can fail, and that failure is idiomatically managed directly and inline, which, when applied to an entire program, significantly improves reliability. If you find yourself doing this often, you can easily write a function to do the grunt work. Probably
> Same deal with a worker pool. Concurrency is great, but instead of providing a robust, well written solution as part of the language itself, it gives you a toy like this
Go's concurrency primitives are very low level. This can be bad, but it can also be good: not all worker pools, for example, need the same set of features.
"I want to interact with a REST API and pull a field out of its response JSON" is an incredibly common workflow, and yet to do that in golang is far from trivial. You need to define serializer types and all sorts of stuff (or you can take a route I've seen encouraged where people to use empty interfaces, which can cause runtime exceptions).
Same deal with a worker pool. Concurrency is great, but instead of providing a robust, well written solution as part of the language itself, it gives you a toy like this https://gobyexample.com/worker-pools (still the most common result on Google) that is only 80% of the way there. Then you find yourself bolting things onto it to cover your features (we need to know if things fail, so let's just add another channel. We also need finality, whelp, another channel it is), and before you know if you have an incomprehensible mess.