Go (styled golang for the purpose of SEO, otherwise finding anything would be impossible) is a pretty great language. It's a language that gets out of your way so that you can just write your applications ASAP. It's a "batteries included" and opinionated ecosystem that brings everything you need to get started.
I'm writing this mostly as a reminder for myself, as a summary of some observations I made along the way. These are just tiny details (but not really gotchas or traps, just generic tips). I'm sure most of you know about these.
Don't use Logrus
Ok, this is related to a generic practice in Go. As a strongly and statically typed language, it's not easy to wriggle your way around datatypes as you would in JS (Node) or PHP for example. With a lack of generics, writing general purpose code as you'd need in a logger or ORM is quite difficult and people resort to reflection.
Logrus uses reflection heavily, which results in heavy allocation count. While generally not a huge problem (depending on code), the reason people choose is performance and while it may sound like micro-optimisation, avoiding reflection matters. If you see something that can use structs without regard for type, it uses reflection and that has an impact on performance.
For example, Logrus doesn't care about the type, though obviously Go needs to know (eventually). Logrus uses reflection to detect the type, which is overhead.
log.WithFields(log.Fields{
"animal": myWhatever,
}).Info("A walrus appears")
What to use I prefer zerolog, but zap isn't bad either. Both boast zero-allocation, which is what you want for a task that should have the smallest possible impact on your application.
Don't use encoding/json
Lots of people recommend using the standard library before looking to anything else. I call the encoding/json
modul an exception. Like the above case, encoding/json
uses reflection. This is not efficient and it can take a toll when writing APIs that return json responses (or any kind of microservice where reading/writing json is important).
Take a look here for some alternatives/benchmarks.
What to use Easyjson is about the top of the pack and it's straightforward. The downside of efficient tools is that they use code generation to create the code required to turn your structs into json to minimise allocations. This is a manual build step which is annoying. Interestingly json-iterator also uses reflection but it's significantly faster. I suspect black magic.
Do not use closures as goroutines
Here's a basic example code:
for i:=0;i<10;i++ {
go func() {
fmt.Println(i)
}()
}
Most people may expect this to result in random printing of numbers 0 to 9, as one would when delegating the task to goroutines.
Actual result: depending on system you will get one or two numbers and a lot of 10's.
Why?
- Closures have access to the parent scope so variables can be used directly. You will not be asked to redeclare though updated linters might warn you about "variable closure capture"
- Go's performance fame is due a lot to the runtime optimisations performed, where it tries to "guess" what you want to do and optimise for some execution paths. During this, it "captures" variables and passes them where they're needed in a way that should theoretically be most efficient (for example, after some non concurrent operations are done to release allocation on some CPU). The result in this case is that the loop may launch goroutines, the the goroutins may receive the value of i which they access from parent scope much later. It's not guaranteed which you will see when executing this code a few times (you will get some random value along side all the 10's).
What to use I don't quite see a reason to use closures like this. It's much more clean and readable to just create a function, whose execution will benefit from a stack of its own. If you do use a closure though for whatever reason, pass the variables! Treat the closure as you would every function.
Cheers!
Top comments (0)