Suppose a function writes a report. It should not need separate implementations for a file, an in-memory buffer, and a network connection. It needs a way to describe the behavior it consumes. An interface is that description: a set of methods a value must provide.
A type implements an interface by having the required methods with matching signatures. There is no implements declaration. This is called implicit implementation. It lets independently written packages cooperate through a small contract. Begin with the behavior a caller needs, rather than inventing a large interface for every method a concrete type happens to have. See the interface tour.
Write to a behavior
Create a fresh module and save this complete program as main.go:
package main
import (
"bytes"
"fmt"
"io"
)
type Labeler interface {
Label() string
}
type Task struct {
Title string
}
func (t Task) Label() string {
return t.Title
}
func writeReport(w io.Writer, item Labeler) error {
_, err := fmt.Fprintf(w, "Task: %s\n", item.Label())
return err
}
func describe(value any) {
labeler, ok := value.(Labeler)
if !ok {
fmt.Println("no label available")
return
}
fmt.Println("label:", labeler.Label())
}
func main() {
var output bytes.Buffer
if err := writeReport(&output, Task{Title: "Read Go"}); err != nil {
fmt.Println("write failed:", err)
return
}
fmt.Print(output.String())
describe(Task{Title: "Practice Go"})
describe(42)
var pointer *Task
var item Labeler = pointer
fmt.Println("pointer nil:", pointer == nil)
fmt.Println("interface nil:", item == nil)
}
Run go run .. Expected output:
Task: Read Go
label: Practice Go
no label available
pointer nil: true
interface nil: false
writeReport does not know about bytes.Buffer. It only needs io.Writer, whose Write method accepts bytes and reports how many were written and any error. Passing &output works because *bytes.Buffer provides that method. A file or another compatible writer could serve the same role. The io.Writer reference defines the full contract, including partial writes.
Labeler is our own small interface. Task satisfies it through its Label method. The function returns write failures to its caller, keeping error handling visible instead of pretending every destination succeeds.
Understand method sets
The method set of Task includes methods with a Task receiver. The method set of *Task includes methods with both Task and *Task receivers. Therefore both can satisfy Labeler here. If Label had a pointer receiver, only *Task would satisfy this interface.
Go's convenient address-taking for a method call on an addressable variable does not mean a value automatically satisfies a pointer-method interface. Interface assignment checks the actual method set. The method-set specification gives the exact rules.
An interface can hold a nil pointer
Think of an interface value as carrying a concrete type and a concrete value. It compares equal to nil only when neither is present. At the end of the example, item carries the type *Task and a nil pointer value. It is therefore a non-nil interface.
Do not call item.Label() in that state: this example's value-receiver method cannot obtain a Task from a nil pointer. Some pointer-receiver methods deliberately handle nil, but callers cannot assume that behavior. This distinction also affects functions returning error: return literal nil for success, rather than a typed nil error pointer. The official nil-interface FAQ explains the common surprise.
Assertions, any, and useful boundaries
any is an alias for the empty interface. It can hold a value of any type, but it does not provide arbitrary methods on that value. The two-result type assertion in describe asks whether the stored value satisfies Labeler. A failed assertion returns ok == false; the one-result form would panic instead.
Use type switches when several concrete cases are genuinely needed. Prefer a typed parameter such as Labeler when the behavior is already known: that lets the compiler check callers. An interface is most useful at a real substitution boundary, such as output, storage, or an external service. You can accept an interface while returning a concrete type whose capabilities remain clear.
Exercise: add another label source
Create type Course struct { Name string } with a Label() string method. Pass a course to writeReport without changing that function. Then change Course.Label to a pointer receiver and adjust the call so it compiles.
Solution guidance
The value-receiver version returns c.Name and allows Course{Name: "Go"}. With a pointer receiver, pass &Course{Name: "Go"}. The expected report is Task: Go because the report owns that prefix. This reveals another design choice: rename the prefix to Item if the function should describe several kinds of labeled object.
Previous: Pointers, Structs, and Methods. Next: Error Handling.

