This page is a golang interface tutorial for beginners who want more than syntax: what an interface means in Go, how types satisfy interfaces without an implements keyword, why that design matters, and where interfaces show up in real APIs. Later sections cover interface parameters (when a function only needs certain behavior), printing dynamic values, any and the empty interface, type assertion and type switch, pointer receiver pitfalls, small interfaces from the standard library, composition, a short contrast with generics, and frequent mistakes. For deeper narrowing rules, see type assertions in Go; for structs and methods, see structs in Go and methods in Go.
Tested on: Go go1.24.4 linux/amd64; kernel 6.14.0-37-generic.
What is an interface in Go?
An interface defines behavior using a set of method signatures. A value of interface type can hold any concrete value whose type provides those methods with matching names and signatures. The compiler checks satisfaction at assign time: you never declare “implements” on the concrete type.
type Expenser interface {
Cost() float64
}If Book and Trip both define Cost() float64, both satisfy Expenser without extra syntax. Naming often follows the -er habit from the standard library (Reader, Writer, Stringer) when the interface describes one capability.
Why interfaces are useful in Go
Interfaces let you write one function or package against a narrow capability instead of a long list of concrete types. Callers can pass different structs (or non-structs) as long as they implement the methods you need. That keeps dependencies pointed inward toward behavior, which is why many Go APIs take io.Reader instead of *os.File specifically.
Basic golang interface example
Here is a minimal go interface example: two concrete types (Rectangle, Circle) share a Shape interface with one method. A single function sums areas over []Shape.
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
}
type Rectangle struct{ W, H float64 }
func (r Rectangle) Area() float64 { return r.W * r.H }
type Circle struct{ R float64 }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func totalArea(shapes []Shape) float64 {
var sum float64
for _, s := range shapes {
sum += s.Area()
}
return sum
}
func main() {
shapes := []Shape{Rectangle{3, 4}, Circle{1}}
fmt.Println(totalArea(shapes))
}You should see one floating-point area (rectangle 12 plus circle π). This is the usual mental model for interfaces in golang: different types, one shared method set, one loop.
How a type implements an interface
Go uses implicit implementation: there is no implements keyword. If type T’s method set contains every method of interface I (with correct signatures), T satisfies I. The concrete type does not import or mention I at all—satisfaction is structural.
That choice keeps packages decoupled: the consumer can define a small interface that existing types already satisfy, instead of forcing library types to implement interfaces declared upstream.
Celsius and Fahrenheit below never name Stringer; they only define String() string, so both satisfy this file’s Stringer interface and can be passed to printTemp.
package main
import "fmt"
type Stringer interface {
String() string
}
type Celsius float64
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
type Fahrenheit float64
func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
func printTemp(s Stringer) {
fmt.Println(s.String())
}
func main() {
printTemp(Celsius(21))
printTemp(Fahrenheit(70))
}You should see two lines with degree-style text (21°C and 70°F). Nothing in the types’ declarations references Stringer—that is implicit implementation.
Interface as a function parameter
A golang function interface parameter means “I need this behavior, not this concrete struct.” Any caller passing a value that implements the interface type-checks cleanly.
package main
import "fmt"
type Speaker interface {
Name() string
}
type Cat struct{ n string }
func (c Cat) Name() string { return c.n }
type Robot struct{ id int }
func (r Robot) Name() string { return fmt.Sprintf("bot-%d", r.id) }
func greet(s Speaker) {
fmt.Println("Hello,", s.Name())
}
func main() {
greet(Cat{"Luna"})
greet(Robot{7})
}greet does not know whether it received a Cat or Robot; it only calls Name. That is the core of polymorphism in Go—without inheritance.
Interface values: concrete value and dynamic type
An interface value is a pair: a dynamic type (the concrete type stored right now) and a dynamic value (the actual data). Assigning a new concrete value to the same interface variable can change both halves. That model explains printing (%T, %#v), type assertion, and subtle nil comparisons below.
The same variable x of type any first holds an int, then a string; %T and %v reflect the change each time.
package main
import "fmt"
func main() {
var x any
x = 42
fmt.Printf("first: type=%T value=%v\n", x, x)
x = "hi"
fmt.Printf("second: type=%T value=%v\n", x, x)
}You should see first: type=int value=42 and second: type=string value=hi.
Print interface values (golang print interface)
For debugging and logs you often need to print interface values without knowing the concrete type. Use fmt verbs: %v for the default format, %#v for a Go-syntax representation, %T for the dynamic type, and plain fmt.Println which also uses default formatting.
package main
import "fmt"
func main() {
var v any = 42
fmt.Println("Println:", v)
fmt.Printf("default=%v type=%T goSyntax=%#v\n", v, v, v)
}You should see Println: 42 plus one line with default=42, type=int, and goSyntax=42. Types that implement fmt.Stringer (String() string) get custom text when printed with those verbs. For dedicated “interface to string” patterns (JSON, strconv, edge cases), see cast to string in Go.
Empty interface and any
interface{} is the empty interface: it places no methods on the method set, so any concrete value satisfies it. Go 1.18 added the predeclared alias any for the same thing—prefer any in new code for readability.
Holding any is useful for APIs that must accept unknown shapes (encoding, frameworks, generic containers). The tradeoff is you lose type-specific operations until you assert or switch on the concrete type; avoid sprinkling any everywhere when a concrete type or a small interface would be clearer.
One any variable can hold an int, a string, and a slice in sequence; describe only knows the concrete shape at runtime via %T.
package main
import "fmt"
func describe(v any) {
fmt.Printf("%T : %v\n", v, v)
}
func main() {
var box any
box = 1
describe(box)
box = "two"
describe(box)
box = []int{3, 4, 5}
describe(box)
}You should see three lines starting with int, string, and []int respectively.
Type assertion
A type assertion x.(T) reinterprets the dynamic value as concrete type T. The single-value form panics if T does not match; the comma-ok form is the safe default:
package main
import "fmt"
func main() {
var v any = "hello"
if s, ok := v.(string); ok {
fmt.Println("string:", s)
} else {
fmt.Println("not a string")
}
}You should see string: hello. Use assertions at boundaries (parsing, plugin-style dispatch), not through every layer of the call stack.
Type switch
A type switch switch v := x.(type) branches on the dynamic type of an interface. It is the usual shape for helpers that handle several concrete kinds.
package main
import (
"fmt"
"strconv"
)
func toText(v any) string {
switch t := v.(type) {
case string:
return t
case int:
return strconv.Itoa(t)
default:
return fmt.Sprint(t)
}
}
func main() {
fmt.Println(toText("x"), toText(7))
}You should see x and 7 on one line. This pattern is clearer than long chains of individual assertions when the set of types is small and fixed.
Interface with struct methods
Structs are the most common types behind interfaces: you attach methods with receivers, and the struct’s method set determines which interfaces it satisfies. See methods in Go for receiver syntax and structs in Go for fields and embedding.
Value receivers (func (t T) M()) and pointer receivers (func (t *T) M()) contribute differently to the method set of T versus *T; that matters for interface satisfaction in the next section.
Dog and Book are different structs; both use a value receiver on Label, so both satisfy Namer and can be passed to show without pointers.
package main
import "fmt"
type Namer interface {
Label() string
}
type Dog struct{ name string }
func (d Dog) Label() string { return "dog: " + d.name }
type Book struct{ title string }
func (b Book) Label() string { return "book: " + b.title }
func show(n Namer) {
fmt.Println(n.Label())
}
func main() {
show(Dog{"Rex"})
show(Book{"Interfaces"})
}You should see dog: Rex and book: Interfaces on separate lines.
Pointer receivers and interface implementation
If an interface requires a method defined only on *T (pointer receiver), both *T and T can still satisfy some interfaces—but a bare value T does not gain pointer-receiver methods in its method set. In practice: when methods use pointer receivers to mutate state or avoid copying large structs, you often store *T in variables of interface type.
package main
import "fmt"
type Greeter interface {
Greet()
}
type S struct{}
func (s *S) Greet() { fmt.Println("hi") }
func main() {
var g Greeter = &S{}
g.Greet()
}If you change the assignment to var g Greeter = S{} (non-addressable zero value cases aside), the program fails to compile because S’s method set does not include Greet when Greet is only on *S. When in doubt, check the method set in go doc or a small compile test.
Small interfaces in Go
Effective Go encourages small, focused interfaces—often one or two methods—sometimes named after the method (Reader, Writer). Three you will use constantly:
io.Reader—Read([]byte) (int, error)io.Writer—Write([]byte) (int, error)fmt.Stringer—String() string
Functions that accept io.Reader work with *os.File, *bytes.Buffer, *strings.Reader, and countless other types without importing each package.
Interface composition
An interface can embed other interfaces to combine requirements. The standard library defines io.ReadWriter as the union of Reader and Writer.
package main
import (
"bytes"
"fmt"
"io"
)
type ReadWriter interface {
io.Reader
io.Writer
}
func use(rw ReadWriter) {
rw.Write([]byte("hi"))
b, _ := io.ReadAll(rw)
fmt.Println(string(b))
}
func main() {
var buf bytes.Buffer
use(&buf)
}You should see hi. Composition keeps shared behavior names consistent across packages.
Interface vs struct
A struct is a bundle of fields (data layout). An interface is a bundle of method signatures (behavior). Beginners coming from class-based languages sometimes define an interface per struct “just in case”; in Go it is more common to start with concrete types and introduce interfaces where abstraction pays off (see the next section).
Interface vs generics (brief)
Interfaces classify behavior: “anything that can Read.” Generics classify types: “a function that works for any T with a known constraint.” Use interfaces when behavior varies at runtime across unrelated types; consider generics in Go when the algorithm is identical and only the type name changes. They complement each other; this article does not dive into generic constraints.
When should you define an interface?
A common rule: define interfaces at the point of use (the consumer), not necessarily next to every concrete implementation. If only one package needs “something storable,” that package can declare a two-method Storer and let database, file, and mock types satisfy it implicitly. Defining large interfaces “up front” often produces unused methods and harder refactors.
Common interface mistakes
- Big interfaces up front — hard to implement and tempts fake methods; start small.
anyeverywhere — hides bugs until runtime; prefer concrete types or narrow interfaces.- Expecting
implements— Go does not work that way; learn method sets instead. - Nil interface confusion — a typed nil interface (
var e error = nil) is not the same as storing a typed nil pointer in a non-empty interface; see the example below. - Pointer receiver mismatch — value
Tvs*Tmethod sets do not always match your interface. - Over-interfaceing — if only one concrete type exists, a plain struct parameter may be simpler.
package main
import "fmt"
type T struct{}
type I interface{ M() }
func (*T) M() {}
func main() {
var i1 fmt.Stringer = nil
fmt.Println("typed nil interface == nil:", i1 == nil)
var p *T = nil
var i2 I = p
fmt.Println("(*T)(nil) stored in I == nil:", i2 == nil)
}typed nil interface == nil: true
(*T)(nil) stored in I == nil: falseThe second interface value carries type *T even though the data word is nil—so comparing i2 == nil is false.
The built-in error interface
Every Go programmer uses the built-in error interface: a single method Error() string. It is a real-world one-method interface—small, ubiquitous, and satisfied implicitly by fmt.Errorf wrappers, errors.Join, and custom types.
package main
import "fmt"
type AppError struct{ Msg string }
func (e AppError) Error() string { return e.Msg }
func main() {
var err error = AppError{"bad input"}
fmt.Println(err)
}You should see bad input. For richer error patterns, follow your project’s style guide and the errors package.
Go interface cheat sheet
| Topic | What to remember |
|---|---|
| Interface | Named method set = behavior contract |
| Implementation | Implicit; method set must match |
| Function parameter | Accept Speaker, not Cat specifically |
| Interface value | Dynamic type + dynamic value |
| Print / debug | %v, %#v, %T, fmt.Stringer |
Empty / any |
any == interface{}; recover type with assertion or switch |
| Type assertion | v, ok := x.(T) to avoid panic |
| Type switch | switch v := x.(type) for several kinds |
| Pointer receivers | *T may be required to satisfy I |
| Composition | Embed interfaces inside interfaces |
| vs struct | Data vs behavior |
| vs generics | Runtime behavior vs compile-time type parameters |
Summary
Interfaces in Go describe behavior through a method set; types satisfy them implicitly when their methods match—no implements keyword. That lets functions accept interface parameters (io.Reader, fmt.Stringer, or small interfaces you define) so callers can pass different concrete types. Each interface value carries a dynamic type and value, which explains printing with %T and %v, safe type assertions, and type switches. Prefer any only where values are truly dynamic; watch nil-interface comparisons and pointer receiver method sets. Define narrow interfaces where they are consumed, use generics when the algorithm is the same but the type name varies, and use cast to string in Go or reflection in Go only when those tools match the problem.
References
- A Tour of Go: Methods and interfaces
- Go by Example: Interfaces
- The Go Programming Language Specification: Interface types
- Effective Go: Interfaces
fmtpackageiopackage

