1. 12
  1. 8

    At least this article should quote the original author @piglei when translating. Original post here (in Chinese): https://www.zlovezl.cn/articles/go-func-argument-patterns/

    Personally I posted several translations on my blog without asking the original author too (which was not something I was proud of for sure). But I always put the URL to the original post upfront.

    1. 2

      Interesting. I had someone ask to translate my blog post to Chinese and I said yes on the condition that we just link to each other. Seems like the way to do it.

    2. 5

      This is good, but it’s missing the “builder” pattern. The functional option pattern ends up repeating the package name over and over:

      pkg.ListApplications(pkg.WithOwner("piglei"), pkg.WithHasDeployed(false))
      

      With the builder pattern, you can just hang all the options as a method chain:

      err := pkg.ListApplications().
          WithOwner("piglei").
          HasDeployed(false).
          Do()
      

      Each of the patterns has its place.

      1. 1

        I can’t give a good reason, but I’ve never liked the builder pattern.

      2. 2

        First off, a common handy function I have in several Go projects I am working on:

        // New returns a pointer to x.
        func New[T any](x T) *T {
        	return &x
        }
        

        If you put it in a package named ptr you can do ptr.New(true) for optional arguments. Since it’s so small I figured there’s really no need to look for a library doing that (I also have things like ptr.DerefOrZero and stuff like that in that package.)

        With that function, it’s not that painful to add new fields and stay backwards compatible.

        But also, there’s an unmentioned alternative you can do which I would probably do in the example given. You can make the zero value useful via enums:

        type DeploymentState string
        
        const (
        	DeploymentStateAny         DeploymentState = ""
        	DeploymentStateDeployed    DeploymentState = "deployed"
        	DeploymentStateNotDeployed DeploymentState = "not_deployed"
        	// (or camelCase or whatever case you prefer)
        )
        

        It seems unlikely in this particular scenario that the enum will change, but I have been burnt more often than not in cases where we have a boolean flag for filtering and need to extend it in some way. Nowadays I usually take the hit up front so that adding in the new alternative in a backwards compatible manner looks prettier for everyone.

        1. 3

          Experience has taught me that booleans are really better expressed as an enum with 2 values in 95% of cases, when performance isn’t critical. Go is lacking exhaustive matching primitives which would make using enum the superior solution in all but the most extreme situations.

          1. 2

            I made a new.Of helper that works like you’re ptr.New if anyone cares: https;//github.com/carlmjohnson/new. I hope it becomes a builtin soon.

          2. 2

            One more, which is pretty common:

            type ApplicationLister struct {
            	Limit int
            	Offset int
            	Owner string
            }
            
            func (a *ApplicationLister) List() []Application { ... }
            
            res := ApplicationLister{Limit: 5, Offset: 0}.List()
            

            This is common with server configs, where the struct is the server instance rather than being a separate config type/instance (with additional hidden fields) that takes configuration from the public field on .Serve() or equivalent.

            It’s also common for packages to offer default wrappers, like

            func List() []Application {
            	return ApplicationLister{}.List()
            }
            

            So we can call pkg.List() or pkg.ApplicationLister{...}.List()