← Go, End to End

Go, End to End

Interfaces

An interface in Go is a set of method signatures. Any type that has all those methods satisfies the interface automatically, there's no `implements` keyword, no declared relationship at all. The compiler figures it out from shape alone.

Implicit satisfaction

This is the single biggest mental shift coming from Java, C#, or C++. You don't write `class Dog implements Animal`. You just write a `Dog` type with the right methods, and it satisfies `Animal` whether or not you ever intended it to.

go · live, editable, runnable

This means an interface can be satisfied by types you don't own and can't modify, including types from the standard library or a third-party package you're just consuming. That's a real strength: you can define your own small interface around someone else's type after the fact.

The empty interface and any

`interface{}` (or its alias `any`, added in Go 1.18) has zero methods, so literally every type satisfies it. It's the closest thing Go has to "accept anything", and it shows up constantly in older code (`fmt.Println` is variadic `...any` under the hood).

The catch: once something is `any`, you've thrown away all compile-time type checking on it. You need a type assertion or type switch to get useful behavior back, and if you get the type wrong at runtime, that's a panic waiting to happen unless you use the safe two-value form.

go · live, editable, runnable

Before generics existed, `any` was often the only way to write a function that worked over multiple types, at the cost of runtime type switches everywhere. Now, if you're writing new code and the operation you need is the same regardless of type, reach for a generic type parameter instead (covered later in this course), and save `any`/type switches for cases where the types genuinely need different handling, like the example above.

Type assertions

A type assertion (`x.(T)`) tells the compiler "I know this value is actually a T, let me use it as one". The single-value form panics if you're wrong. The two-value form (`v, ok := x.(T)`) never panics, it just tells you whether the assertion succeeded.

go
var v any = "hello"

s := v.(string)         // ok, panics if v isn't actually a string
s, ok := v.(string)      // safe: ok is false instead of panicking on a mismatch

Default to the two-value form unless you have a very good reason to believe the assertion can never fail and want a hard crash if that assumption is ever wrong. A stray panic from an unchecked assertion deep in a request handler is a bad way to find out your assumption broke.

Interface composition

Interfaces can embed other interfaces, building bigger contracts out of smaller ones. The standard library's `io` package is the textbook example, and it's worth internalizing because you'll see it everywhere.

go
type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

type ReadWriter interface {
	Reader
	Writer
}

Nothing implements `ReadWriter` on purpose, it just falls out automatically the moment a type has both a `Read` and a `Write` method with those exact signatures. Files, network connections, in-memory buffers: they all end up satisfying `ReadWriter` for free.

Accept interfaces, return structs

This is the closest thing Go has to an official API design commandment. A function's parameters should ask for the smallest interface that covers what the function actually needs, not a specific concrete type, so callers have maximum freedom to pass in anything that fits. But the function's return type should usually be a concrete struct, not an interface, so callers get full access to everything that type offers, not just whatever a narrower interface happened to expose.

go · live, editable, runnable

Notice `countBytes` never asked for a `*strings.Reader` specifically, just "something with a Read method". That means it works unchanged with a file, a network socket, an in-memory buffer, or anything else that happens to implement `Read`. Writing the parameter as `any` concrete type would have locked callers out of all of those, for no benefit.

A quick gut check

  • If you're about to write "implements" in a comment to explain a relationship, stop, Go has no such declaration, and the comment is doing work the compiler doesn't verify.
  • If a function only ever gets called with one concrete type in practice, an interface parameter is probably premature, a concrete type is simpler and just as flexible until a second caller shows up.
  • If you're returning an interface from a constructor-style function "to allow swapping implementations later", ask whether that's a real, current need or speculative flexibility you're paying complexity for today.
Try it yourself: define your own interface around a stdlib type

Define a `Stringer`-shaped interface yourself (it exists in fmt already, but build your own to see the mechanism), and pass in a type that never mentions your interface at all:

go · live, editable, runnable

Employee never references Named anywhere in its own definition. Try adding a second type of your own with a differently-shaped Name method (different return type, or extra parameter) and confirm it fails to compile as a Named.