Gopher says
Interfaces are Go’s version of contracts. Instead of saying “this type
inherits from that class,” Go says “this type has the right methods.” No
implements keyword needed - if the methods match, the contract is satisfied
automatically.
In baton-junction, we satisfy SDK interfaces like ResourceSyncerV2,
ResourceProvisioner, and AccountManager without ever declaring
“implements” - we just define the right methods and the SDK picks them up
automatically.
What an Interface Is
An interface defines a set of method signatures. Any type with those methods satisfies it:
type ResourceSyncer interface {
ResourceType(ctx context.Context) *v2.ResourceType
List(ctx context.Context, ...) ([]*v2.Resource, string, error)
} Key rules:
- Interfaces contain only method signatures - no fields, no implementations
- Satisfaction is implicit - implement the methods and you’re done
- One type can satisfy many interfaces without knowing about any of them
Implicit Satisfaction in the Baton App
This is where interfaces stop being abstract and start being practical. baton-junction’s userBuilder satisfies the SDK’s ResourceSyncer interface just by having the right methods:
type userBuilder struct {
client *client.Client
}
func (o *userBuilder) ResourceType(_ context.Context) *v2.ResourceType {
return userResourceType
}
func (o *userBuilder) List(ctx context.Context, _ *v2.ResourceId, attrs resource.SyncOpAttrs,
) ([]*v2.Resource, *resource.SyncOpResults, error) {
users, nextCursor, err := o.client.GetUsers(ctx, cursor)
...
}
No implements ResourceSyncer anywhere. The compiler checks automatically.
Polymorphism: One Interface, Many Types
Three different structs - userBuilder, groupBuilder, roleBuilder - all satisfy the same interface. The connector returns them in a single slice. This uses a slice composite literal - the type followed by braces containing the elements:
[]InterfaceType{
value1,
value2,
value3,
} The []Type{...} syntax means “create a slice of this type with these elements.” The first {} is not separate from the second - the entire []Type{...} is one expression.
func (m *Connector) ResourceSyncers(_ context.Context) []connectorbuilder.ResourceSyncerV2 {
return []connectorbuilder.ResourceSyncerV2{
newUserBuilder(m.client),
newGroupBuilder(m.client),
newRoleBuilder(m.client),
}
}
The SDK iterates this slice and calls List() on each - without knowing the concrete types. Adding a fourth resource type means creating a new builder and adding it to the slice. No existing code changes.
Type Assertions
An interface hides the concrete type - but sometimes you need to peek inside. Maybe you have a slice of syncers and only some of them can provision entitlements. When you need to discover what’s behind an interface, use a type assertion with the comma-ok pattern:
type provisioner interface {
Grant(ctx context.Context, principal *v2.Resource, entitlement *v2.Entitlement) (annotations.Annotations, error)
Revoke(ctx context.Context, grant *v2.Grant) (annotations.Annotations, error)
}
gs, ok := groupSyncer.(provisioner)
if ok {
gs.Grant(ctx, user, entitlement)
}
If ok is true, the concrete type also has Grant and Revoke methods. If not, no panic - just false.
The any Type
Sometimes you need to accept any type - for example, when writing a generic HTTP helper that populates different response structs. The empty interface interface{} (aliased as any) is satisfied by every type:
func (c *Client) doGet(ctx context.Context, path, cursor string, response any) error {
// response can be ListResponse[User], SingleResponse[Role], etc.
}
Use any sparingly - it trades compile-time safety for flexibility.
Why Slice-of-any Looks Strange
You might encounter expressions like []any{1, "hello", true} or []interface{}{}. These look confusing because of the doubled braces, but they follow the same []Type{values} composite literal pattern. The first pair of braces is part of the type name (interface{}), and the second pair holds the elements:
[]any{1, "hello", true}
This creates a slice where each element can be any type. It’s the same as []interface{}{1, "hello", true}. You’ll see this in map values (map[string]any{}) and when building generic data structures.
baton-junction demonstrates two key Go idioms:
Keep interfaces small. The standard library’s io.Reader has just one method: Read(p []byte) (n int, err error). Similarly, io.Closer has just Close() error. Go lets you compose small interfaces into larger ones - io.ReadCloser combines both, so any type with Read() and Close() satisfies it automatically. The test’s provisioner follows the same principle: two methods, nothing more.
Define interfaces where they’re consumed. The provisioner interface is defined in the test file, not in groups.go. The consumer describes what it needs; the implementer doesn’t need to know the interface exists.
Quick Check
If groupSyncer has Grant() and Revoke() methods, what does the ok variable hold after: gs, ok := groupSyncer.(provisioner)?
Exercise: Implement an Interface
Define a Describer interface with one method: Describe() string. Then create two structs - UserResource (with Name and Email string fields) and GroupResource (with Name string and MemberCount int fields). Implement a Describe() method on each that returns a formatted string. The printAll function in the starter code already calls Describe() - your structs need to satisfy the Describer interface.
Gopher says
Interfaces unlock Go’s polymorphism - no inheritance trees, no implements
keyword. Just methods and contracts. The compiler does the rest.
In baton-junction, userBuilder, groupBuilder, and roleBuilder all
satisfy different SDK interfaces just by having matching methods - no
boilerplate registration required.
Next: collections!
Next Lesson
Collections

