Hacker News new | comments | show | ask | jobs | submit login
Useful Techniques in Go (arslan.io)
165 points by luu 683 days ago | hide | past | web | 45 comments | favorite



'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.


Sometimes 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)


This 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.


I 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.


[flagged]


Because you don't agree with this: 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.


It 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.


It'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.


Look, 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.


#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.


Great 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


Author 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 :)


not strange since stringer was not written yet.

https://github.com/golang/tools/commit/2c5c896732f6bfdcc0360...


Regarding #6, starting with 1 is better than starting with 0, but even better is using negative values: 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

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

#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.


> but even better is using negative values

Honest question: Why?


Most 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/54789eff385780c54254f822e0...

with the definitions of constants here:

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.


Thanks.


The 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")


  type 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


> 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.


I 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.


Except that the spec states that the 0 value is "more special".

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"


Huh, I didn't know Go has a concept of implicit default value. In that aspect, zeroing out in struct literals also makes sense.

However I would say that while default zero is better than garbage value, failing to compile is even better than default zero. (e.g. the compiler refuses to compile `var i int` in the first place) But I guess this is a matter of preference.


If the code failed to compile unless you initialized all values, you could never add a field to an existing type without updating all cases where you were creating an instance. This would basically make refactoring impossible in most real-world Go projects. You could potentially solve this problem by forcing all object creation to be through special constructor functions (like Java and Scala do), but that is not the Go way of doing things.


Yup, I'm much in favor of using a constructor, or even a trait, if such a breakage can be problematic, because I believe types are part of an API (so they should have been opaque types in the beginning). But I now understand what an idiomatic Go way is.


All ML-like languages as well as Swift and Rust, force you to initialize all values in the struct. Adding new fields isn't a problem in practice, because most of the time structs that you wanted to add fields to had private fields already to begin with, so they're using a constructor.

The nice thing about this approach, of course, is that they can omit all the complexity of "zero values" from the language.


That's why you use specialized constructors.


    > What I disagree is the "zeroing out" part. I don't want 
    > 0 or empty string to be more "special" than ordinary 
    > values.
Sentinel zero values have been part of Go since day 1, and leveraged to provide some very nice semantics. See e.g. bytes.Buffer. It's a fundamental and useful part of the language.


>Letting the compiler decide what values are suitable if I omit them can be a cause of potential bug.

The alternative is not to let you omit them, or to nil them. Both worse.


But nilling them is exactly what Go does no? It initialises every field to their default/zero. Preventing their omission seems markedly better than the alternative, if you want to allow partial initialisations you can provide a constructor function or an explicit defaults construction.


Except that without implicit declaration, how do you bring constructor variance into check? For example, if you have an object/class that has a lot of options available, you would have to create a huge number of constructor functions, or other extensions to simply initialize an instance...

As I'm more familiar with JS than golang, it seems to me, with the current method, I can do something like...

    var inst = new CustomObject(param1, param2, {
       option15: 'bar'
       ,...
    });
Where the first two parameters are required, but the last one can be a structure that represents available/optional options as a singular type... This would be much cleaner than supporting every variation of overloads as part of object construction. Let alone where a type can simply be used directly.


In Rust you usually use either method chaining (CustomObject::new(param1, param2).option15('bar')) or the struct update syntax (CustomObject { param1: param1, param2: param2, option15: 'bar', ..DEFAULT_CUSTOM_OBJECT }) for this. No extra complexity of "zero values" necessary (and the concept of a zero value would be incoherent in Rust anyway).


But you have either the complexity of multiple closures, or complex mutability (side effects) with that strategy, depending on how you approach your implementation. In either case the outcome could be less than ideal, and have more chance of unexpected results.

Is this...

    var obj = new CustomObject(...);
    ...
    obj.option15('bar');
The same as

    var obj = new CustomObject(...).option15('bar');
IE: does calling option15 create a new closure/space that is separate from the original object (more functional separation) or does it mutate the original object?

I tend to prefer to limit mutations as much as possible in my code... it really just depends on your needs.


There is neither mutability nor multiple closures involved in the record update syntax.


There are many thing they could have done. Maybe handle both cases the same, using 'in-order' matching for the first unnamed case. Or since they like regenerating source, spitting out :

    Valid source:
    t := T{"example", 123, ""}
    or
    t := T{"", 123, "example"}
Using some kind of unification of set(String, Int, String) with set("example"::String, 123::Int)



I 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.


The 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.


Zero values in struct initialization (and other places) is a routinely encountered behavior in go.

http://golang.org/ref/spec#The_zero_value


I'd present it as options going either way so the reader knows which to choose. There have been times when I would use either.


Re #7: Why have a function wrap it at all if all you're going to do is return the function?


It'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.


Lots 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.


Yes, 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 plugin. Especially with full oracle support, it's a killer.


5) I've replied on another comment, but stringer was not written back then. Totally agree on that one :)

6) Makes sense, but starting it with +1 makes it really easy to see if it's initialized or not (if you care about or need the knowledge of external explicit initialization).

11) Thanks akavel :)


These are excellent tips.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | DMCA | Apply to YC | Contact

Search: