Interface Type Declarations
How to declare an interface type in Go, specify method sets, and understand the implicit satisfaction that makes Go's type system distinct.
An interface type in Go is a named collection of method signatures. You declare one to define a contract for behavior without saying anything about how that behavior gets carried out. Any concrete type that possesses all the methods listed in the interface automatically satisfies that interface, with no extra keywords needed. This page covers the mechanics of declaring an interface, what goes into a method set, and how a type comes to meet the contract.
What an Interface Declaration Looks Like
The syntax starts with the type keyword, the interface name, the word interface, and a block containing method signatures.
type Shape interface {
Area() float64
Perimeter() float64
}
A method signature inside an interface block consists of the method name, an optional parameter list, and any return types. Parameter names are not required, though including them often makes the interface more readable. The signatures above say: any type that wants to be a Shape must have an Area method that takes no arguments and returns a float64, and a Perimeter method that also returns a float64.
Interface names and conventions:
Single-method interfaces in Go typically end in “er” — Reader, Writer, Stringer. Multi-method interfaces usually use nouns that describe the abstraction: Shape, ReadWriter, Handler.
An interface declaration does not allocate memory, create a value, or provide any implementation. It simply introduces a new type whose values can hold any concrete value that meets the method requirements.
Method-Set Specification
The set of methods declared inside the interface block is called the method set. The interface type itself provides no implementation; it only describes which methods must exist on a concrete type for that type to be assignable to the interface.
Each method signature in the method set must include:
- The method name, case‑sensitive and following Go’s identifier rules.
- A parameter list, even if empty:
(). - Zero or more return types. Multiple return values are written inside parentheses:
(int, error).
type FileHandler interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
Close() error
}
Here FileHandler specifies three methods. The parameter names p, n, and err are meaningful for documentation but the compiler only checks the types. You could rewrite the interface as:
type FileHandler interface {
Read([]byte) (int, error)
Write([]byte) (int, error)
Close() error
}
Both declarations are functionally identical. The first is usually preferred because the names explain what each argument does.
Parameter names do not affect satisfaction:
A concrete type that implements Read with a parameter named buf instead of p still satisfies the interface. Only the types matter.
A method set can also be empty. The interface interface{} (or its alias any since Go 1.18) defines zero methods, which is the empty interface.
Implicit Interface Satisfaction
In Go, a concrete type does not explicitly declare that it implements an interface. It simply needs to have all the methods the interface requires, with matching signatures. The compiler verifies this automatically.
Take the Shape interface from earlier and two structs:
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
Both Circle and Rectangle now satisfy Shape. No implements keyword, no interface list in the struct definition. You can prove it by assigning them to a Shape variable:
var s Shape
s = Circle{Radius: 5}
fmt.Println(s.Area()) // 78.539...
s = Rectangle{Width: 4, Height: 3}
fmt.Println(s.Perimeter()) // 14
The compiler checks the assignment at compile time. If a method is missing or a signature doesn’t match, the program won’t build.
Compile‑time safety:
If you attempt to assign a type that lacks a required method, you get a clear compile error. No runtime surprises from missing methods.
This implicit mechanism is one of Go’s most distinctive features. It lets you define interfaces that existing types — even types from packages you don’t control — already satisfy, without modifying the original code.
Value Receivers vs Pointer Receivers
The method set of a concrete type includes:
- All methods defined with a value receiver for both
Tand*T. - Methods defined with a pointer receiver only for
*T— not forT.
This directly affects whether a type satisfies an interface. Consider a variation where Area and Perimeter are defined on a pointer receiver:
type Square struct {
Side float64
}
func (s *Square) Area() float64 {
return s.Side * s.Side
}
func (s *Square) Perimeter() float64 {
return 4 * s.Side
}
Now *Square satisfies Shape, but Square does not. Assigning a plain Square value to a Shape variable will not compile.
var sh Shape
sq := Square{Side: 4}
sh = sq // compile error: Square does not implement Shape (Area method has pointer receiver)
sh = &sq // OK
Pointer receiver mismatch is a common compile‑time block:
Beginners often write methods on a pointer receiver without realizing it prevents the value type from satisfying the interface. If you see a “does not implement” error that lists a method you definitely wrote, check whether the receiver is a pointer and whether you’re using a value where a pointer is expected.
The same rule applies in the opposite direction: if a method uses a value receiver, both the value and pointer types satisfy the interface. That’s why Circle and Rectangle worked smoothly — their methods used value receivers, so both Circle and *Circle satisfy Shape.
Using an Interface Variable After Declaration
Once you have an interface type, you can declare variables of that type. A freshly declared interface variable holds a zero value of nil — it has neither a concrete value nor a concrete type.
var s Shape
fmt.Println(s == nil) // true
Assigning a concrete value to the interface stores both the value and its dynamic type inside the interface variable. Calling a method on the interface variable dispatches to the concrete type’s implementation.
s = Circle{Radius: 10}
fmt.Printf("Type: %T, Area: %f\n", s, s.Area())
// Type: main.Circle, Area: 314.159265
The key idea is that after declaration, the interface variable acts as a container that can hold any type satisfying the method set.
Common Mistakes with Interface Declarations
- Mismatched method signature — if the concrete type’s method returns
(float64, error)but the interface expects onlyfloat64, satisfaction fails. The compiler error tells you which signature is wrong. - Using unexported method names across packages — method names that begin with a lowercase letter are package‑private. An interface in another package cannot refer to them directly. Keep interface methods exported (uppercase first letter) if you intend cross‑package satisfaction.
- Forgetting that interface variables can be nil — calling a method on a nil interface causes a runtime panic. This isn’t a declaration issue, but it’s a direct consequence of how interface zero values work.
- Attempting to create an instance of an interface — you cannot write
s := Shape{}because an interface has no implementation. You must assign a concrete type that satisfies it.
Interface Declaration and Package Visibility
Interfaces follow Go’s export rules. An interface name starting with an uppercase letter is exported and visible to other packages. An interface name starting with a lowercase letter is unexported, visible only within its own package.
package shapes
// Exported — usable from other packages.
type Shape interface {
Area() float64
}
// Unexported — only shapes and its sub‑packages can reference it.
type internalValidator interface {
validate() error
}
This pattern lets you expose public behavioral contracts while keeping internal implementation contracts hidden — a clean boundary that helps manage complexity in large codebases.
Summary
Interface type declarations give you a way to name a set of methods and treat any type that has those methods as belonging to that named abstraction. The declaration itself is lightweight: a type keyword, a name, the word interface, and a block of signatures. There is no code generation, no runtime machinery beyond what’s needed to store a concrete value later.
The real power comes from implicit satisfaction. You write concrete types that happen to have the right methods, and the compiler enforces the contract at the point of assignment. This decoupling lets you design around behavior rather than around concrete implementations — and it’s the foundation for everything that follows in this chapter.