This is a repository of how forward compatibility breaks within go. This demonstrates how we can break compatibility, and how the different ways effect users of the API, and SDK.
The definition for the scope of this document is: When a user of the the API and the SDK upgrades only the API and not the SDK, the program will continue to work.
Why is that important? In go the tooling assumes any minor version change is forward compatible, and will update uses of the API to the highest version used in any dependency. This means if you specify API & SDK v1.0.0, but you have a dependency that uses API V1.1.0 the go tooling will update to use API v1.1.0 and not change the SDK.
There are three main directories: api
, sdk
, and client
.
- The
api
directory contains an API that we will make an addition to. - The
sdk
directory contains a matching SDK that won't be updated, it could, but the client will be able to upgrade the API (think via a dependency) with or without an SDK update. - The
client
directory contains a client that uses the SDK, and indirectly the API.
Under each of the main directories there are three subdirectories: abstract
, direct
and side
.
- The
abstract
directory contains an example if we embed the interface in the SDK structs. This will allow the client to upgrade, but any use of the new API will panic - The
direct
directory contains an example if we directly add the new method to the API. This will allow clients to upgrade, but not compile. - The
side
directory contains an example if we create a second interface that users of the API can assert to. This will allow clients to upgrade, but all uses of the new API require assertions, and checks.
The examples discussed below are from the client-v2 tag
By embedding the interface the program does compile, but will panic when the new method is called. This can be mitigated by embedding a NoOp implementation.
$ go run .
v1
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x20 pc=0x457ce4]
goroutine 1 [running]:
github.com/madvikinggod/intexplore/sdk/abstract.(*blank).V2(0x4649c3?)
<autogenerated>:1 +0x24
main.main()
/home/aaron/projects/intexplore/client/abstract/main.go:9 +0x7f
exit status 2
By directly adding the new method to the API the program will not compile. The other problem is that the error message doesn't directly point to the solution that the SDK needs to be updated.
$ go run .
# github.com/madvikinggod/intexplore/sdk/direct
/home/aaron/go/pkg/mod/github.com/madvikinggod/intexplore/sdk/direct@v1.0.1/blank.go:12:9: cannot use &blank{} (value of type *blank) as type "github.com/madvikinggod/intexplore/api/direct".Blank in return statement:
*blank does not implement "github.com/madvikinggod/intexplore/api/direct".Blank (missing V2 method)
By creating a second interface the program will compile, but the user of the API will need to assert to the new interface. This is the most flexible, but Interface Smuggling has other usability issues. See the ResponseController replacing the Hijacker
and Flusher
etc interfaces.
$ go run .
v1
blank is not V2
if b, ok := b.(side.BlankV2); ok {
println(b.V2())
} else {
println("blank is not V2")
}