Golang Interface Tutorial: Examples, Implementation, and Type Assertion

Learn interfaces in Go with simple examples, including method sets, implicit implementation, interface parameters, empty interface, any, type assertion, printing interface values, composition, pointer receivers, and common mistakes.

Published

Updated

Read time 12 min read

Reviewed byDeepak Prasad

Golang Interface Tutorial: Examples, Implementation, and Type Assertion

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.

go
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.

go
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))
}
Output

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.

go
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))
}
Output

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.

go
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})
}
Output

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.

go
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)
}
Output

You should see first: type=int value=42 and second: type=string value=hi.


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.

go
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)
}
Output

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.

go
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)
}
Output

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:

go
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")
	}
}
Output

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.

go
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))
}
Output

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.

go
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"})
}
Output

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.

go
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()
}
Output

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:

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.

go
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)
}
Output

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.
  • any everywhere — 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 T vs *T method sets do not always match your interface.
  • Over-interfaceing — if only one concrete type exists, a plain struct parameter may be simpler.
go
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)
}
text
typed nil interface == nil: true
(*T)(nil) stored in I == nil: false

The 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.

go
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)
}
Output

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


Frequently Asked Questions

1. What is a golang interface?

An interface type is a set of method signatures; a concrete type implements it if it has those methods with matching signatures. Go has no implements keyword—implementation is implicit and decided by the method set.

2. How does a go interface differ from a struct?

A struct is a fixed layout of named fields (data). An interface describes behavior (methods). Functions can accept an interface to work with any type that provides that behavior without naming the concrete struct.

3. What is the difference between any and interface{} in Go?

They are equivalent: any is a built-in alias for interface{}, the empty interface with no required methods. Both can hold any concrete value; you recover specific operations with type assertion or a type switch.

4. When should I use a type assertion vs a type switch?

Use a two-value assertion v, ok := x.(Concrete) when you expect one type; use switch v := x.(type) when several concrete types are possible, which is the usual pattern for helpers that format or dispatch on dynamic values.

5. Why does my interface value compare not equal to nil even though the pointer is nil?

An interface value holds a type and a value; assigning a typed nil pointer such as (*T)(nil) to a non-empty interface still stores the concrete type *T, so the interface value is not nil even when the data word is nil. Compare to typed nil interfaces like var e error = nil where both halves are unset.
Antony Shikubu

Systems Integration Engineer

Highly skilled software developer with expertise in Python, Golang, and AWS cloud services.