Level 4 /

Interfaces

The passenger car, dining car, and mail car look completely different. But they all fit on the same track because they share the same wheel gauge and coupling. Gopher lays three sets of blueprints side by side on the table: different purposes, different shapes, same rail.

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:

keywordinterface namemethod signature (no body)
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:

slice of interface typeelements (different concrete types)
[]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.

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!

Explore the related connector code files

Next Lesson

Collections

Continue