Settings

Theme

Reusable and type-safe options for Go APIs

derekchiang.com

79 points by derekchiang 8 years ago · 35 comments

Reader

sethammons 8 years ago

I find I don't care for implementing the Options paradigm. More often then not, the things I've implemented have required parameters, and sometimes many of them. Mixing required and options often feels kludgy. I feel the Options method requires lots of documentation referencing, as opposed to more discoverable (via your editor) constructors. I tend to use explicit parameters or a struct with validators where every field is expected to be explicitly set.

This is not to say that using Options is wrong. Some things are highly customizable and having too many constructors would not be an appropriate solution. Worse would be a struct based config where you only set some fields some times. I just don't find myself creating these kinds of constructs. My most configurable things are usually server instances.

For those who do use the Options paradigm and find it useful, what are you creating?

  • chimeracoder 8 years ago

    I have to say I agree. I came to Go from functional languages (primarily Lisp), so you'd think that the "Functional Options for Friendly APIs" pattern would appeal to me, but I've never seem an implementation that I've actually liked to use.

  • eternalban 8 years ago

    > More often then not, the things I've implemented have required parameters, and sometimes many of them.

    The disconnect for OP here is that generalized systemic approach (think e.g. Java Beans for one of the earliest attempts) is necessary for the container-component architectures [but arguably un-necessary for application specific approaches (which has informed your experience.)]

    Go, imho, is a poor fit for the container-component paradigm. It certainly can meet the initial requirements (context + options) but the lack of first class metadata facilities will limit the possibilities (sans code generation) in that architectural space.

    [pre-emptive p.s.: "container" here /does not/ mean process level composition ala Docker, etc.]

  • jopsen 8 years ago

    I personally prefer something like:

    > type Options struct { ... }

    > func myMethod(arg1 string, options Options) {...}

    Then have people pass in "Options{}" and let the zero-values imply default.

    But most of the time, if you are making things complex, you're missing the whole point of golang. The innovation in golang is it's simplicity, if you miss that, then you might as well code C++ (which have more features for these kinds of things).

sfifs 8 years ago

I've been using Go as my preferred language for almost a decade now. Hackish "clever" solutions like these coming from designers of the language like Rob Pike and Dace Cheney simply indicate that the language is hitting its limits of expressivity and needs to start thinking hard about overloading which is the right solution to problems like this.

  • oelmekki 8 years ago

    Rob Pike and Dave Cheney know way better than me, but I can't help to think they're trying to be "easy rather than simple", here.

    Rob doesn't explain why a config struct is not good for him in his reference article, and Dave is saying that's because 1/ it needs to be passed even when it's empty and 2/ its zero values may be a trouble, giving the example of explicitly setting `Port` to 0 to let OS selecting first free port available.

    I would say that I have absolutely no problem with passing an empty option struct : that's simple. I know what is going on, I don't have to check sources for three methods to understand it.

    Regarding the zero value problem, the example of port selection seems incredibly an edge case. Most of the time, if some option is numeric and can have a special feature, -1 will be used rather than 0 (like it's often the case when passing a limit to specify "no limit"). For the port problem, I would have no problem passing an `AutoselectFreePort bool` option.

    But then again, maybe the pattern seems complicated to me because it's new. We'll see with time.

    • tomjakubowski 8 years ago

      The zero value problem would be a non-issue if Go had a built-in ?T type ("option T").

      • ctz 8 years ago

        That's problematic, thanks to golang choosing to have always-nullable reference types. "option T*" would therefore be a tristate type, unless it was somehow specialised into representing nullability (obviously a major breaking change).

        • weberc2 8 years ago

          If mostly everyone uses Option instead of `nil`, the problem approaches a vanishing point. Not perfect, but still a massive improvement. Of course, generics won't be added until Go 2 anyway, at which point the community is free to make a breaking change, though I doubt they'll ditch `nil`.

    • everdev 8 years ago

      For the zero value problem, a pointer is usually used rather than an "identifier value" like -1.

      var price *float64

      if price == nil {

        // Handle empty value
      
      }
      • sfifs 8 years ago

        This works if you can guarantee the variable will not ever change after being set which is not unreasonable for configuration options.

        However if you are dealing with variables that can mutate in a concurrent environment, you might have a race condition if you pass by pointer reference rather than by value

  • bsaul 8 years ago

    I had the same feeling, and then i tried to implement the same solution in swift (which has more evolved constructs for enums and option sets), but it's actually not easy to do as well, if you want to handle all those constraints at the same time:

    1/ a potentially big number of options, without creating functions with a huge list of parameters

    2/ options with associated values (revision: int)

    3/ reusable options between different operations, with type safety preventing wrong combination.

    => 1/ prevents you from using function overloading and default parameters

    => 2/ prevents you from using optionset (they're just bit masks)

    and

    => 3/ prevents you from using regular sets or arrays as operation parameter.

    I haven't spent more than half an hour trying to find a solution, so maybe there's a smart trick that would work, but it won't be an obvious solution either.

    • fauigerzigerk 8 years ago

      I realise that you tried to work with the same constraints as the Go solution for the sake of argument.

      But the whole rationale for having 3/ in the first place isn't valid in Swift, so it seems to me that the obvious Swift solution is to use an enum per operation.

      • bsaul 8 years ago

        It does makes quite a bit of sense, especially in the case where all those enums end up being processed by one single private function, such as "configureServer(config: ConfOption) ". (i'm refering to the linked post by Dave cheney here)

        I spent a bit more time on the problem, and now i'm pretty sure it is also not solvable "cleanly" in swift, because it actually would need to model some kind of constraints over the accepted values of a type for a parameter.

        Something like get(opt: Options<restrictedTo [.prefix, .revision]>... )

        Otherwise you'll need to manually create a subenum, and manually compare cases from the original one with the sub one.

  • discordianfish 8 years ago

    Why do you find this hackish? I find that pretty straight forward given the constraints. You might prefer a more expressive language but that comes at a cost.

jopsen 8 years ago

Once you accept that the golang type system can't enforce everything your life will be easier.

Use rust or Haskell if you want a powerful type system that can do fancy things.

golang is great, but you have to accept that some things are runtime errors, and that's fine, it keeps things simple. How often are options given conditionally anyways? Simple tests will cover this in most cases.

  • tapirl 8 years ago

    No languages are perfect. Yes Go type system can't enforce everything, but it enforce many things. And Go is still language in evolving.

    • jopsen 8 years ago

      My point was that complex type systems also have down sides. Even if you strongly favor algebratic type systems, you have to admit they are harder to learn (at-least initially).

      The upside with golang is that it's simple and easy to adopt; tiny learning curve, and code + APIs are intended to be simple.

      If you are messing around with complex patterns in golang to get more compile-time correctness checks, you are most likely not keeping it simple. And thus, have missed the most important upside to golang.

    • gdfhhgddhjj 8 years ago

      These are all true things about any language on earth, except perhaps the evolving part.

robmccoll 8 years ago

How is it not cleaner and simpler to just use something of a builder pattern?

   srv = NewServer(addr).WithTLS(crt,key).WithRateLimit(30).WithPool(10).Start()
   if srv.Err() {
     // Handle
   }
  • everdev 8 years ago

    In Go, chaining is not idiomatic. For one, it becomes more challenging to determine where the error occurred if you want to try to handle the error instead of failing. Also, it requires each function in the chain to detect if an error has already occurred.

    Interfaces are generally a better solution.

    • eternalban 8 years ago

      Clearly a fluid API can not return a 2+ tuple as its results so the error issue you note is a non-issue.

      The clean way to deal with errors in fluid APIs is to have a terminal .init() that returns an error (which can also specifically note which options are "illegal", etc.)

  • jopsen 8 years ago

    Or

      NewServer(Options{Addr: addr, RateLimit 30, Pool: 10})
    
    IMO, keep it simple and remove the optional features if possible is also a good choice. If my golang API is complex, then I might aswell have done it in C++ :)
  • eikenberry 8 years ago

    How is it cleaner and simpler? It reads about the same to me.

tdrd 8 years ago

The downside to this solution is that the concrete option types are exported. I believe it's possible to return unexported types from exported functions, which would solve this, but that golint complains about that pattern.

Have you suggested this to the etcd maintainers?

  • derekchiangOP 8 years ago

    That's a good point, and I've since updated the blog post and the code to use anonymous interfaces instead.

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection