Yeah we have a large golang monolith here and it’s sometimes nice to be able to bring in packages as something shorter, at least in my specific context.
panic() is the equivalent of the exception mechanism many languages use to great effect. Idiomatically it’s a last resort, but it’s a superior mechanism in many ways (e.g. tracebacks for debugging, instead of Go’s idiomatic ‘here’s an error message, good luck finding where it came from’ default.)
Go’s idiomatic ‘here’s an error message, good luck finding where it came from’
I think the biggest problem here is that too often if err != nil { return err } is used mindlessly. You then run in to things like open foo: no such file or directory, which is indeed pretty worthless. Even just return fmt.Errorf("pkg.funcName: %s", err) is a vast improvement (although there are better ways, such as github.com/pkg/errors or the new Go 1.13 error system).
I actually included return err in a draft of this article, but decided to remove it as it’s not really a “feature” and how to effectively deal with errors in Go is probably worth an article on its own (if one doesn’t exist yet).
it’s pretty straightforward to decorate an error to know where it’s coming from. The most idiomatic way to pass on an error with go code is to decorate it, not pass it unmodified. You are supposed to handle errors you receive after all.
if err != nil {
return fmt.Errof("%s: when doing whatever", err)
}
not the common misassumption
if err != nil {
return err
}
in fact, the 1.13 release of go formally adds error chains using a new Errorf directive %w that formalises wrapping error values in a manner similar to a few previous library approaches, so you can interrogate the chain if you want to use it in logic (rather than string matching) .
It’s unfortunate IMO that interrogating errors using logic in Go amounts to performing a type assertion, which, while idiomatic and cheap, is something I think a lot of programmers coming from other languages will have to overcome their discomfort with. Errors as values is a great idea, but I personally find it to be a frustratingly incomplete mechanism without sum types and pattern matching, the absence of which I think is partly to blame for careless anti-patterns like return err.
You can now use errors.Is to test the error type and they added error wrapping to fmt.Errorf. Same mechanics underneath but easier to use. (you could just do a switch with a default case)
I greatly prefer the pithy, domain-oriented error decoration that you get with this scheme to the verbose, obtuse set of files and line numbers that you get with stack traces.
As to struct tags, one more issue I see with them is that the naming rules used therein form a global namespace. For example, when multiple package lay claim on a tag “db”, there’s often no way to use them both on a single struct. A possible workaround could be to make tag name a parameter in a handler pkg, but it may still be tricky then, and anyway I’m not sure I’ve ever seen such a feature in real life.
As to init vs. init-like var+funcs, I am actually somewhat on the fence. In my eyes, one advantage of init not mentioned in the article is that it’s easier to grep if you want to find all of them. Whereas with var+func it is significantly harder, esp. when var blocks are used. I also find init kinda more “in your face” explicit, emitting stronger vibes of “I’m bad, please try to refactor me out.” That said, I mostly dislike both of the solutions; whenever I saw them in a codebase, it was usually with some surprise unexpected behavior. Hm, I now realized they basically both sound a code smell signal to me, a yellow light about quality of a codebase. So, both would be FOLRs to me, I guess.
As to struct tags, one more issue I see with them is that the naming rules used therein form a global namespace. For example, when multiple package lay claim on a tag “db”
That’s a valid point, but I have not seen this be a practical problem. Have you encountered this issue? I’d prefer to keep this list as short and concise as possible, focusing only on practical problems.
Re: init(); if you want to minimize package-level variables then a tool to scan for them is probably better than grep -r '^func init()? That will catch everything, including var foo = .... Maybe something like it already exists?
I don’t recall particular circumstances where this matter with struct tag naming occurred to me now, sorry. I changed employer since then, and the codebase I currently work on is much smaller. I just wanted to mention what I thought noteworthy, in case someone else would be interested too; what you include on your list is your call, I don’t care much whether you want to include it or not. To make it clear, I definitely enjoyed reading it as is, esp. the FOLR term you mention is a good one.
As to init… a tool maybe exists, maybe not, I dunno. But, you mentioned practical yourself… grep is the practical approach — I can be fairly sure it will be there whenever I need it, and I will know how to use it; I can even write a quick combo with awk to show the contents of all init funcs everywhere. And as to the “globals tool,” we still don’t even know if it exists in the first place.
Nice list, but also a bit surprising as I found #1 and #2 to be kinda common in a lot of projects. So maybe they’re needed so much that “last resort” is a bit too much? Or maybe a real lack of feature in the language?
struct tags are never really “needed”, outside of (un)marshalling. I have no idea why people use it for validations and DB relations and whatnot. When I was programming Ruby I noticed that people often used features like monkey patching (A Ruby FOLR) when it clearly wasn’t required. I guess it’s one of those laws of programming: “features will be used, whether it’s appropriate or not”.
interface{} is more complex, but it’s not needed nearly as often as it’s used. I’ve “prevented” the use of interface{} (and reflection) in many code reviews by offering alternatives. A lot of new Go programmers come from dynamic languages such as Python, JavaScript, etc. so they’re not always used to thinking with a type system and try to work around it (I did this myself). Other people stubbornly claim that it “ought to work as if it has generics like in C# or Java”, which is not very helpful.
I liked this an agree with most of it. Thanks for writing the post.
One is forced to use import names when importing two packages with the same suffix, for me it was “crypto/rand” and “math/rand”
Yeah we have a large golang monolith here and it’s sometimes nice to be able to bring in packages as something shorter, at least in my specific context.
I’d love to see the equivalent list for C++!
You can probably make a list for many languages or complex systems (PostgreSQL has Don’t do this).
panic()
is the equivalent of the exception mechanism many languages use to great effect. Idiomatically it’s a last resort, but it’s a superior mechanism in many ways (e.g. tracebacks for debugging, instead of Go’s idiomatic ‘here’s an error message, good luck finding where it came from’ default.)I think the biggest problem here is that too often
if err != nil { return err }
is used mindlessly. You then run in to things likeopen foo: no such file or directory
, which is indeed pretty worthless. Even justreturn fmt.Errorf("pkg.funcName: %s", err)
is a vast improvement (although there are better ways, such as github.com/pkg/errors or the new Go 1.13 error system).I actually included
return err
in a draft of this article, but decided to remove it as it’s not really a “feature” and how to effectively deal with errors in Go is probably worth an article on its own (if one doesn’t exist yet).it’s pretty straightforward to decorate an error to know where it’s coming from. The most idiomatic way to pass on an error with go code is to decorate it, not pass it unmodified. You are supposed to handle errors you receive after all.
not the common misassumption
in fact, the 1.13 release of go formally adds error chains using a new Errorf directive
%w
that formalises wrapping error values in a manner similar to a few previous library approaches, so you can interrogate the chain if you want to use it in logic (rather than string matching) .It’s unfortunate IMO that interrogating errors using logic in Go amounts to performing a type assertion, which, while idiomatic and cheap, is something I think a lot of programmers coming from other languages will have to overcome their discomfort with. Errors as values is a great idea, but I personally find it to be a frustratingly incomplete mechanism without sum types and pattern matching, the absence of which I think is partly to blame for careless anti-patterns like
return err
.You can now use
errors.Is
to test the error type and they added error wrapping tofmt.Errorf
. Same mechanics underneath but easier to use. (you could just do a switch with a default case)I guess you mean
but yes point taken :)
Sure, but in other languages you don’t have to do all this extra work, you just get good tracebacks for free.
I greatly prefer the pithy, domain-oriented error decoration that you get with this scheme to the verbose, obtuse set of files and line numbers that you get with stack traces.
I built a basic Common-Lisp-style condition system atop Go’s
panic
/defer
/recover
. It is simple and lacking a lot of the syntactic advantages of Los, and it is definitely not ready for prime time, at all, but I think maybe there’s a useful core in there.But seriously, it’s a hack.
As to struct tags, one more issue I see with them is that the naming rules used therein form a global namespace. For example, when multiple package lay claim on a tag “db”, there’s often no way to use them both on a single struct. A possible workaround could be to make tag name a parameter in a handler pkg, but it may still be tricky then, and anyway I’m not sure I’ve ever seen such a feature in real life.
As to init vs. init-like var+funcs, I am actually somewhat on the fence. In my eyes, one advantage of init not mentioned in the article is that it’s easier to grep if you want to find all of them. Whereas with var+func it is significantly harder, esp. when var blocks are used. I also find init kinda more “in your face” explicit, emitting stronger vibes of “I’m bad, please try to refactor me out.” That said, I mostly dislike both of the solutions; whenever I saw them in a codebase, it was usually with some surprise unexpected behavior. Hm, I now realized they basically both sound a code smell signal to me, a yellow light about quality of a codebase. So, both would be FOLRs to me, I guess.
That’s a valid point, but I have not seen this be a practical problem. Have you encountered this issue? I’d prefer to keep this list as short and concise as possible, focusing only on practical problems.
Re: init(); if you want to minimize package-level variables then a tool to scan for them is probably better than
grep -r '^func init()
? That will catch everything, includingvar foo = ...
. Maybe something like it already exists?I don’t recall particular circumstances where this matter with struct tag naming occurred to me now, sorry. I changed employer since then, and the codebase I currently work on is much smaller. I just wanted to mention what I thought noteworthy, in case someone else would be interested too; what you include on your list is your call, I don’t care much whether you want to include it or not. To make it clear, I definitely enjoyed reading it as is, esp. the FOLR term you mention is a good one.
As to init… a tool maybe exists, maybe not, I dunno. But, you mentioned practical yourself…
grep
is the practical approach — I can be fairly sure it will be there whenever I need it, and I will know how to use it; I can even write a quick combo withawk
to show the contents of all init funcs everywhere. And as to the “globals tool,” we still don’t even know if it exists in the first place.Nice list, but also a bit surprising as I found #1 and #2 to be kinda common in a lot of projects. So maybe they’re needed so much that “last resort” is a bit too much? Or maybe a real lack of feature in the language?
struct tags are never really “needed”, outside of (un)marshalling. I have no idea why people use it for validations and DB relations and whatnot. When I was programming Ruby I noticed that people often used features like monkey patching (A Ruby FOLR) when it clearly wasn’t required. I guess it’s one of those laws of programming: “features will be used, whether it’s appropriate or not”.
interface{}
is more complex, but it’s not needed nearly as often as it’s used. I’ve “prevented” the use ofinterface{}
(and reflection) in many code reviews by offering alternatives. A lot of new Go programmers come from dynamic languages such as Python, JavaScript, etc. so they’re not always used to thinking with a type system and try to work around it (I did this myself). Other people stubbornly claim that it “ought to work as if it has generics like in C# or Java”, which is not very helpful.Yeah maybe I was overstating it, but if you do anything with web, json (un)marshalling will be there.
eh, not really