8

Is adding a variadic parameter to an existing Go function a breaking change?

For example:

// Old function
func Foo(a int)

// Updated to:
func Foo(a int, params ...string)

Callers of the API can omit the new parameter, so I would think the API is backwards-compatible.

Can anyone provide an example where a user of the old API could not use the new API without changing their code?

Duncan Jones
  • 67,400
  • 29
  • 193
  • 254
  • 1
    Given that `foo` is unexported: Nobody cares. It it would be exported: It is a braking change. What that means for semver can be judeged by semverionistas. – Volker Mar 14 '19 at 13:20
  • I've removed references to semver, since that part is subjective. The question is now focused on whether this could break something, for someone. I've also made `foo` into `Foo`, since it only makes sense to discuss exported functions. – Duncan Jones Mar 14 '19 at 15:26

1 Answers1

13

I. Changing functions

Calling them will continue to work without modification, but since the function signatures do not match, that may easily break some code.

For example (try it on the Go Playground):

func Foo(a int)                    {}
func Foo2(a int, params ...string) {}

func main() {
    var f func(int)

    f = Foo
    f = Foo2 // Compile-time error!

    _ = f
}

The line f = Foo2 produces a compile-time error:

cannot use Foo2 (type func(int, ...string)) as type func(int) in assignment

So this is a backward incompatible change, don't do it.

The above example gave a compile-time error, which is the lucky / better case, but there may also be code that would only fail at runtime (non-deterministic if / when that happens), like in this example:

func Foo(a int)                    {}
func Foo2(a int, params ...string) {}

func main() {
    process(Foo)
    process(Foo2) // This will panic at runtime (type assertion will not hold)!
}

func process(f interface{}) {
    f.(func(int))(1)
}

Calling process(foo) succeeds, calling process(foo2) will panic at runtime. Try it on the Go Playground.

II. Changing methods

Your question was directed at functions, but the same "problem" exists with methods too (when used as method expressions or method values, for example see golang - pass method to function).

Additionally, this may break implicit interface implementations (it may make types not implement interfaces), like in this example (try it on the Go Playground):

type Fooer interface {
    Foo(int)
}

type fooImpl int

func (fooImpl) Foo(a int) {}

type fooImpl2 int

func (fooImpl2) Foo(a int, params ...string) {}

func main() {
    var f Fooer

    f = fooImpl(0)
    f = fooImpl2(0) // Compile time error!

    _ = f
}

Because signatures don't match, fooImpl2 does not implement Fooer, even though fooImpl does:

cannot use fooImpl2(0) (type fooImpl2) as type Fooer in assignment:
  fooImpl2 does not implement Fooer (wrong type for Foo method)
      have Foo(int, ...string)
      want Foo(int)
icza
  • 389,944
  • 63
  • 907
  • 827
  • since he is talking about callers of the api i would say it is not a backward incompatible change for the api users, since the old way of calling the api would still work – Pizza lord - on strike Mar 14 '19 at 13:16
  • 2
    @Pizzalord It may continue to work in some cases, but it will break in other cases. The examples in the answers prove that. It depends on how it is used, but all uses cases in my answer are valid. – icza Mar 14 '19 at 13:17
  • Most changes (even those accepted by Go, according to their backward-compatibility promise) have the potential to break _something_. Adding a field to a struct can break the ability to copy between structs of different types, but with the same fields, for example, but this is routinely done by Go. So whether this breakage matters for semver is, IMO, mostly a matter of opinion. – Jonathan Hall Mar 14 '19 at 14:43
  • 2
    @Flimzy I agree, somewhat true. But Go's interface design is special and very powerful, and if the function being changed this way is a method, that would break implicit interface implementations–which in my opinion is a serious change. Extending a struct with a new field does not imply signature changes. – icza Mar 14 '19 at 14:49
  • I agree. Changing a function/method signature is a lot more significant than adding a struct field. – Jonathan Hall Mar 14 '19 at 15:20
  • 1
    Thanks for the answer. I've simplified my question a little, since it was getting people a bit angsty over semver ambiguities. However, you've still answered the new question - it's possible for someone to use my old API and be tripped up by the change. So it's a break change and I'll handle it as such. Thank you. – Duncan Jones Mar 14 '19 at 15:28