3

I believe adding variadic function to a regular function or method is breaking change based on this post. But what about adding variadic parameters to a constructor function? like adding Functional Options.

By using semver this code is v1.0.0

type Foo struct{}

// constructor with default behavior
func NewFoo() *Foo {
    return &Foo{}
}

Adding a variadic parameter

type Foo struct{}

type Option func(&Foo)

// constructor with option
func NewFoo(opts ...Option) *Foo {
    // process option before return
    // ...
    return &Foo{}
}

Old code still fine when calling the constructor function and no one assigns constructor to a variable and passing around constructor to another function like in this case.

So, in the above code, should I increment the major version to v2.0.0 or the minor one to v1.1.0?

billyzaelani
  • 567
  • 1
  • 5
  • 18
  • 1
    If I call `NewFoo` without arguments, does the behaviour differ from the old version? Passing in options as varargs is a very common thing, but in the old version, were these options set through different functions, are they still available, and do they still work in the same way? Basically: have you changed the behaviour of `NewFoo()`, and are there changes in how you interact with `*Foo`? If so: v2, if not: v1.1.0 – Elias Van Ootegem Feb 01 '20 at 13:25
  • If you are replacing `Foo()` with `Foo(...)` and there use cases where this interface may be exposed via DLL's, it's a breaking change. – jwdonahue Feb 01 '20 at 21:48
  • Does Go have delegates? `NewFoo()` appears to be a factory, therefore any delegates based on `NewFoo()` will reference the wrong type. This smells like a breaking change to me, but I don't know much about Go. – jwdonahue Feb 01 '20 at 21:56

2 Answers2

2

I think either's fine, because this is a grey area. In principle you're breaking backwards compatibility because the signature of the function changes, but in practice it's unlikely that callers will be affected.

It's comparable to adding fields to a struct that have sensible default zero-value interpretations for the new fields. Code that uses these structs may behave differently (for example, if they look at the size of the struct, or if they use reflect), but it's not pragmatic to call this a breaking change, unless you expect people are doing these edge-case things.

Paul Hankin
  • 54,811
  • 11
  • 92
  • 118
1

In Go, should I increment the major version to v2.0.0 or the minor one to v1.1.0?


In Go, the NewFoo function types

v1.0.0:

type Foo struct{}

func NewFoo() *Foo {}

and

v2.0.0:

type Foo struct{}

type Option func(*Foo)

func NewFoo(opts ...Option) *Foo { }

are distinct types. That makes it a breaking change, an update to the major version number.


However, it's not really a major change, just an extension, so update the minor version number. Use similar but distinct function names. For example, NewFoo and NewFooOpts.

v1.1.0:

type Foo struct{}

func NewFoo() *Foo {
    return NewFooOpts()
}

type Option func(*Foo)

func NewFooOpts(opts ...Option) *Foo {
    var foo Foo
    for _, opt := range opts {
        // handle opt
        _ = opt
    }
    return &foo
}

Playground: https://play.golang.org/p/HcN1WCi0YK4

Use the comments documenting the functions to provide guidance to the user as to which function to use. For example, perhaps function NewFoo is deprecated in favor of function NewFooOpts.


No one assigns constructor to a variable and passing around constructor to another function.


Since some one does, the statement is false.

peterSO
  • 158,998
  • 31
  • 281
  • 276
  • The second case is indeed not breaking change, but it's bloated the public API. I agree with [Paul Hankin](https://stackoverflow.com/users/1400793/paul-hankin) answer. This is a grey area and both have pros and cons. – billyzaelani Feb 02 '20 at 08:30