7 ms·
Useful Techniques in Go
- modulus1 11y ago'Use tagged literals for struct initializations'... I'd rather the compiler help me make sure i've updated all uses appropriately when I add a new field.
- politician 11y agoThat's where I stopped reading.
- farslan 11y agoBecause you don't agree with this: http://golang.org/doc/go1compat http://golang.org/doc/go1compat ? And there is even an example of how a new field called `Zone` was introduced and not using tagged literals would break the existing code. I don't understand why you said I stopped reading despite these both two facts just written there.
- politician 11y agoIt is reckless to suggest that one can arbitrarily add fields to structs because they've used tagged literals with the implication that this won't bite people with difficult to find default value errors down the line. It's a poor solution to a problem that's far better handled with a constructor function.
- NateDad 11y agoIt's not reckless. Go has zero values for a reason. When writing code, you need to take into consideration what it means if one or more of the values in your struct is set to the zero value. It's pretty trivial to say "ok, I added this new field Foo. If Foo is the zero value, do the same thing the code used to to before Foo existed". Then your package will be 100% backwards compatible for people that used struct literals. Obviously, that's not always possible, but often times it is.
- politician 11y agoLook, suppose you go ahead and follow this advice instead of using a constructor function. What have you got? First, you'll have to update each-and-every single function that operates on the struct to deal with zero-values with ambiguous semantics (i.e. is it zero because that's meaningful in my domain, or is it zero because Johnny forgot to update the allocation). If you'd used a constructor function, then you could've resolved the meaning of those default values and been done with it. Second, the next time you add a new field, you'll have to deal with a potentially complex interplay between default values and what the code actually needs to do. Third, the absence of compiler errors fails to notify third-party users of your library how the semantics of your struct has changed, with all of the long-nights-of-debugging that that will entail.
- farslan 11y agoSometimes you don't use the field everywhere. Instead you add a new field to extend a functionality, with a proper new method/function. So if you use tag literals your existing code will still compile safely, and the tests will pass too (assuming you didn't change the functions where the struct were used). In a very large code base, not using tagged literals will just break anything. Not sure you want it (for the case of extending). Removing will already break it so no problems there. (Disclaimer: I wrote the blog post)
- modulus1 11y agoThis is one of the trouble spots I have with Go. Say I have 3 members that must be set, and 5 optional members. Go's solution is to make the 3 critical members private so that someone using this struct will probably figure out they should use a construction function. And doing this means replacing all code that creates my struct with calls to a 'New' function. With all this friction, people aren't likely to do the right thing. At least in C++ with public members I can initialize appropriately in the constructor. Of course littering my code-base with public members would be bad practice in the first place.
- NateDad 11y agoI used to worry about that, too, but in practice it rarely causes problems. The constructor function will be grouped with the type it returns in Godoc, and auto-complete generally will show both type Foo and func NewFoo when you start typing "Foo". And generally you place the constructor function next to the type it creates in the code as well.... it all contributes to it not really being a problem.
- barosl 11y agotype T struct { Foo string Bar int Qux string } t := T{"example", 123} // doesn't compile t := T{Foo: "example", Bar: 123} // OK I find this behavior somewhat surprising. Contrary to the author's claim, I would expect both to fail to compile. Reading the Go specification, it seems to be a valid behavior. > An element list that contains keys does not need to have an element for each struct field. Omitted fields get the zero value for that field.[1] I understand zeroing out the "unused" fields is common in low level programming, but Go's behavior in this specific case feels a bit too implicit for my taste. [1] https://golang.org/ref/spec https://golang.org/ref/spec
- NeutronBoy 11y ago> Contrary to the author's claim, I would expect both to fail to compile. I wouldn't - the first example is undefined (is 123 Bar or Qux? I guess technically it should be Bar as without quotes it's an int... but it's not unusual for int's to be cast into strings on assignment) and I would expect it to fail. The second example explicitly states which variable is being assigned. edit: Thinking more about it, I guess it could go either way, but it's just one of those things that once you do it once you know it.
- barosl 11y agoI understand that in the latter case, what fields are being assigned is clearer (albeit the former case can also adopt a similar order-based strategy). What I disagree is the "zeroing out" part. I don't want 0 or empty string to be more "special" than ordinary values. Letting the compiler decide what values are suitable if I omit them can be a cause of potential bug.
- Vendan 11y agoExcept that the spec states that the 0 value is "more special". https://golang.org/ref/spec#The_zero_value https://golang.org/ref/spec#The_zero_value I personally like it this way, and it's better, imho, to have a "set everything not specified to 0" step rather then "leave whatever garbage was there"
- jaytaylor 11y agoLots of gems of good advice here! Not sure I agree about #7 though, as I prefer the more explicit and log friendly approach of not returning function call results directly.
- Everlag 11y ago#8 is pretty great in that you can assign methods to custom types but not builtins. As an example, say I have []Card. If I want to sum all cards with a certain color then I could Sum(cards []Card, color string) but that is not great. Rather, I could use type Cards []Card and then name a method (cards *Cards) Sum(color string) int The benefit is that any []Card can now have Sum called on it without any explicit casting. I've been using this and find it to be clean.
- ainar-g 11y agoGreat article, especially #2. I found it strange how they didn't mention the stringer tool[1] in #5 though. [1]: https://godoc.org/golang.org/x/tools/cmd/stringer https://godoc.org/golang.org/x/tools/cmd/stringer
- igungor 11y agonot strange since stringer was not written yet. https://github.com/golang/tools/commit/2c5c896732f6bfdcc0360b5afe8b3345c6b2e758 https://github.com/golang/tools/commit/2c5c896732f6bfdcc0360...
- farslan 11y agoAuthor here. It was not written back then :) However it was used so much Rob decided to write a tool to auto generate the String method. So it's now very easy to generate and and it's also uses a much more efficient algorithm to return the string :)
- 4ad 11y agoRegarding #6, starting with 1 is better than starting with 0, but even better is using negative values: http://play.golang.org/p/ES8nNv4PeW http://play.golang.org/p/ES8nNv4PeW Also regarding #6, the author didn't say it, but with iota is really trivial to create power of two constants, so you can combine them: http://play.golang.org/p/rzD3Vl0C4q http://play.golang.org/p/rzD3Vl0C4q For #5, he should have mentioned the stringer tool to maintain the list of strings for you: https://godoc.org/golang.org/x/tools/cmd/stringer https://godoc.org/golang.org/x/tools/cmd/stringer #10 really depends. There are many cases where you want to wrap your map, but there are many other cases (more cases dare I say), where it doesn't really matter because you only use the map (which is embedded in s struct) in very few places (basically some methods of that struct). The example also fails to synchronise reads. A more important case that is not mentioned is that if you often initialise once and read many times, without ever writing again, you don't need that kind of wrapper in those scenarios. And you also don't want that kind of wrapper even if you read and write often, but read more than one value at a time. That hurts performance, but can even limit your available semantics. With an explicit lock you can arrange so that your map presents a consistent snapshot at all times.
- sambeau 11y ago> but even better is using negative values Honest question: Why?
- 4ad 11y agoMost small values in programs are positive (lengths, counts, etc), so it's possible to write a value to something that should be initialised to a constant by mistake, and you'll never know, since the ranges overlap. But if you use negative values, and you write a positive value, when you see it, you'll know. Of course this is a very trivial thing, but it helped me many times. Sometimes it's better to use typed constants with a custom type, then the Go type system will help you and not let you assign things of a different type. Note that I only said sometimes, in many cases it's better to use untyped constants (or use primitive types rather than custom integers), and positive integers because you can calculate the constant corresponds to some value from a delta, or from some other criteria, like here: https://github.com/golang/go/blob/54789eff385780c54254f822e09505b6222918e2/src/cmd/internal/obj/arm64/asm7.go#L890 https://github.com/golang/go/blob/54789eff385780c54254f822e0... with the definitions of constants here: https://github.com/golang/go/blob/54789eff385780c54254f822e09505b6222918e2/src/cmd/internal/obj/arm64/7.out.go#L260 https://github.com/golang/go/blob/54789eff385780c54254f822e0... Another thing useful when debugging is using constants with non-overlapping peculiar ranges, rather than small integers, then simply by seeing a value in a debugger (or in a print of a value without a String() method), you'll know where that comes from.
- akavel 11y agoYes, and no. #3 - ("T{A:1, B:2} is better than T{1, 2}") - usually yes, and you'd want to write it multi-line for readability etc (see #4); but there are times, when you exactly want to benefit from the fact, that T{1, 2} is verified for completeness. And sometimes it can read better in table tests. (Sometimes. Only sometimes.) #5 - as others said already, you can be smart and spare yourself some dumb work here and use golang.org/x/tools/cmd/stringer + go generate. #6 - even better: start from 0, and make sure the 0 value is correct as the default value for unitialized variable/field! E.g.: const ( Stopped State = iota Running Rebooting Terminated ) // That said, this is assuming the "Stopped" can also mean "pre-running". // Otherwise, add a named "Uninitialized" state, or something. #7 - I believe sometimes yes, sometimes not. Especially in longer functions, for the sake of "no surprises", it might be easier for readers to just keep multiple boring (read: regular) "return 0, err" blocks, and final "return x, nil". Also, it's then slightly easier to add more code to such a function (in growing codebase), and/or refactor it. Still, for cases when this results in a concise one-liner, totally yes! #9 - as Author notes at the end, "This approach has the disadvantage that it pushes out the indentation and makes it harder to read. Again seek always the simplest solution." In my opinion, nice trick to know, but usually a "x := NewContext(...); defer x.Close()" or similar is the standard idiom. NOTE: Especially for locks, I'd say you won't be adding anything to the block in future (see also: YAGNI). On the contrary, you might actually want to change it to a RWLock at some point, and then modify only some of the uses, and then the func would actually make it more annoying. #10 - I'd say, only when you need it. If you don't need the lock, just use the map. If you need the lock... usually, I'd think you probably already have some higher level meaning for the "map", so I'd suggest to already wrap it in a proper type name & higher-level interface. "type FooRegistry { ... }; func (r * FooRegistry) Register(...)" etc. (And, actually, probably don't add the Delete() yet, until you really need it.) And probably you already have more complexity at this point that you'll want to nicely encapsulate in those funcs. That said, all of the above is just my subjective opinion, too. "#11" - By the way: if you're able to force yourself to use vim, absolutely have a look at the Author's https://github.com/fatih/vim-go https://github.com/fatih/vim-go plugin. Especially with full oracle support, it's a killer.
- farslan 11y ago
- vog 11y agoI disagree with #3. If I change a data structure I want the compiler to tell me exactly which parts of the code I should rethink. I do not want it to silently initialize new fields with automatic values, as that may invalidate some of my invariants. Confusingly, it seems that the author is aware us this issue, as he acknowledges exactly this issue #6, and provided a recommendation for easier debugging of such situations.
- LanceH 11y agoI'd present it as options going either way so the reader knows which to choose. There have been times when I would use either.
- 2mur 11y agoZero values in struct initialization (and other places) is a routinely encountered behavior in go. http://golang.org/ref/spec#The_zero_value http://golang.org/ref/spec#The_zero_value
- NateDad 11y agoThe idea is that, when you add a new field to the struct, the behavior should not change if that field is given its zero value. Thus, all the rest of your code continues to work as it did before. If you change the code such that the zero value of that field makes the code behave differently, then yes, it's your responsibility to go find all the places that use the struct and update them. However, thanks to static typing and nice namespaces, that's trivial.
- ccannon 11y agoThese are excellent tips.
- ccannon 11y agoRe #7: Why have a function wrap it at all if all you're going to do is return the function?
- farslan 11y agoIt's just an example. Of course the function has many other statements before the returning function which is not shown, otherwise it doesn't make sense of course.
- voidlogic 11y agoThe shown label example for #2 is only one option, you could also use a simple loop: for loop := true; loop; { select { case <-time.After(time.Second): fmt.Println("hello") default: loop = false } } fmt.Println("ending")