Fundamentals 22 min read

Understanding Duck Typing and Interface Implementation in Go

This article analyzes Go's duck typing and interface mechanisms, explaining how interfaces are defined, implemented, and converted at runtime, including details of underlying structures like iface and eface, method sets, pointer vs value receivers, and type assertions, supplemented with code examples and assembly insights.

Xueersi Online School Tech Team
Xueersi Online School Tech Team
Xueersi Online School Tech Team
Understanding Duck Typing and Interface Implementation in Go

This article provides an in-depth analysis of Go's duck typing concept and how the language implements interfaces, based on the Go 1.12.12 source code and runtime behavior on amd64 machines.

1. Duck Typing

1.1 What is Duck Typing

Duck typing is a style of type inference that focuses on external behavior rather than internal structure. As the classic saying goes, "If it walks like a duck and it quacks like a duck, then it must be a duck." In Go, this principle is applied through interfaces.

1.2 Go's Duck Typing

Go achieves duck typing via interfaces. Unlike dynamic languages that check types only at runtime, or static languages that require explicit implementation declarations, Go interfaces are satisfied implicitly when a concrete type provides the required methods.

2. Overview

2.1 Interface Types

An interface is an abstract type that exposes only method signatures without revealing the underlying data layout. When you have a variable of an interface type, you cannot know its concrete type, only the methods it supports.

2.2 Interface Definition

Go uses the interface keyword to define an interface. For example:

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

Interfaces can be composed by embedding other interfaces, e.g., io.ReadWriter embeds io.Reader and io.Writer .

2.3 Implementing an Interface

If a concrete type implements all methods required by an interface, the type implicitly satisfies that interface. Example:

type Runner interface { Run() }

type Person struct { Name string }

func (p Person) Run() { fmt.Printf("%s is running\n", p.Name) }

func main() {
    var r Runner
    r = Person{Name: "song_chh"}
    r.Run()
}

2.4 Interface and Pointer Receivers

Methods can have either value or pointer receivers. A method with a pointer receiver cannot be called on a value of the concrete type when the interface expects that method, leading to compile‑time errors.

type Runner interface { Run(); Say() }

type Person struct { Name string }

func (p Person) Run() { fmt.Printf("%s is running\n", p.Name) }
func (p *Person) Say() { fmt.Printf("hello, %s", p.Name) }

func main() {
    var r Runner
    r = &Person{Name: "song_chh"} // works
    r.Run(); r.Say()
    // r = Person{Name: "song_chh"} // compile error: Person does not implement Runner (Say has pointer receiver)
}

2.5 nil and non‑nil Interface Values

An interface value is nil only when both its dynamic type and dynamic value are nil. Assigning a nil pointer to an interface makes the interface non‑nil because the dynamic type is set.

var r Runner
fmt.Println(r == nil) // true
var p *Person
fmt.Println(p == nil) // true
r = p
fmt.Println(r == nil) // false

2.6 Interface Value Internals

An interface value consists of two parts: the concrete type (dynamic type) and the concrete value (dynamic value). Only when both are nil does the interface compare equal to nil.

3. Implementation Principles

3.1 iface

The non‑empty interface is represented by the iface struct:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

The itab links the concrete type to the interface type and stores method pointers.

3.2 eface

The empty interface ( interface{} ) is represented by the simpler eface struct:

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

3.3 Converting a Concrete Type to an Interface

The compiler generates an itab and calls runtime.convT2I to allocate memory for the concrete value and populate the interface:

func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
    t := tab._type
    x := mallocgc(t.size, t, true)
    typedmemmove(t, x, elem)
    i.tab = tab
    i.data = x
    return
}

3.4 Interface‑to‑Interface Conversion

When converting between interfaces, the runtime uses runtime.convI2I which either reuses the existing itab if the target interface has the same method set or creates a new one.

func convI2I(inter *interfacetype, i iface) (r iface) {
    tab := i.tab
    if tab == nil { return }
    if tab.inter == inter { r.tab = tab; r.data = i.data; return }
    r.tab = (inter, tab._type, false)
    r.data = i.data
    return
}

3.5 Type Assertions and Switches

Go provides two ways to extract a concrete value from an interface: a type assertion ( v := x.(T) or v, ok := x.(T) ) and a type switch ( switch v := x.(type) { ... } ). A failed assertion without the comma‑ok form triggers a panic.

4. References

《Go程序设计语言》, 机械工业出版社

“golang中interface底层分析”, 简书

“浅谈 Go 语言实现原理”, draveness.me

“深度解密Go语言之关于interface的10个问题”, 博客园

Further articles in this series will continue to explore Go runtime internals such as channels, slices, and maps.

GoRuntimeduck typinginterface{}Type Assertion
Xueersi Online School Tech Team
Written by

Xueersi Online School Tech Team

The Xueersi Online School Tech Team, dedicated to innovating and promoting internet education technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.