Deep Dive into Go Interfaces: Duck Typing, Receivers, Interface Internals, and Polymorphism
The article thoroughly examines Go’s interface system, explaining static duck typing, the differences between value and pointer receivers, the internal iface/eface structures, how interfaces are constructed and converted, compiler implementation checks, type assertions, runtime polymorphism, and contrasts these mechanisms with C++ abstract‑class interfaces.
This article provides a comprehensive exploration of Go interfaces, covering duck typing, the distinction between value and pointer receivers, the internal representation of interfaces (iface and eface), interface construction, compiler checks, type conversion, assertions, polymorphism, and a comparison with C++ interfaces.
1. Go and Duck Typing
The article starts by quoting the classic duck‑typing definition and explains that Go, although a static language, supports duck typing through its interface mechanism. An example in Python is shown:
def hello_world(coder):
coder.say_hello()In Go, the same idea is expressed by defining an interface and implementing the required method:
type IGreeting interface {
sayHello()
}
func sayHello(i IGreeting) {
i.sayHello()
}Two concrete types implement the method:
type Go struct{}
func (g Go) sayHello() {
fmt.Println("Hi, I am GO!")
}
type PHP struct{}
func (p PHP) sayHello() {
fmt.Println("Hi, I am PHP!")
}Calling sayHello(golang) and sayHello(php) demonstrates Go's static duck typing.
2. Value vs. Pointer Receivers
The article explains that methods can have either a value receiver or a pointer receiver. It shows that both value and pointer types can call either kind of method, but the compiler treats them differently. Example code:
type Person struct {
age int
}
func (p Person) howOld() int {
return p.age
}
func (p *Person) growUp() {
p.age += 1
}
func main() {
qcrao := Person{age: 18}
fmt.Println(qcrao.howOld()) // 18
qcrao.growUp() // modifies original
fmt.Println(qcrao.howOld()) // 19
stefno := &Person{age: 100}
fmt.Println(stefno.howOld()) // 100
stefno.growUp()
fmt.Println(stefno.howOld()) // 101
}A table summarises the behavior of value and pointer receivers for both value and pointer callers.
3. iface vs. eface
The internal structures are presented:
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}Details of itab (interface table) and _type are shown, including fields such as inter , _type , hash , and the function pointer slice fun . The article includes assembly‑level diagrams and the layout of these structures.
4. Interface Construction
The process of converting a concrete value to an interface value is illustrated with the compiler‑generated function runtime.convT2I64 . The source of that function is reproduced:
func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
var x unsafe.Pointer
if *(*uint64)(elem) == 0 {
x = unsafe.Pointer(&zeroVal[0])
} else {
x = mallocgc(8, t, false)
*(*uint64)(x) = *(*uint64)(elem)
}
i.tab = tab
i.data = x
return
}The article walks through the generated assembly for a simple program that assigns a Student value to a Person interface, showing how the itab address is loaded and how runtime.convT2I64 is called.
5. Compiler Interface Implementation Checks
It demonstrates the common pattern used to verify at compile time that a type implements an interface:
var _ io.Writer = (*myWriter)(nil)
var _ io.Writer = myWriter{}If the methods are missing, the compiler emits an error indicating the missing implementation.
6. Interface Conversion (convI2I)
When converting one interface to another, the runtime uses runtime.convI2I :
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 = getitab(inter, tab._type, false)
r.data = i.data
return
}The helper getitab looks up or creates an itab in a global hash table, ensuring that the conversion is fast after the first lookup.
7. Type Assertions and Switches
The article explains safe and unsafe type assertions, showing panic‑inducing and safe forms:
var i interface{} = new(Student)
// unsafe, will panic
s := i.(Student)
// safe
s, ok := i.(Student)
if ok {
fmt.Println(s)
}It also demonstrates a type‑switch that distinguishes nil , Student , and *Student values, printing the addresses of the variables at each stage.
8. Polymorphism via Interfaces
Using a Person interface with methods job() and growUp() , the article shows how both Student and Programmer types satisfy the interface, enabling polymorphic calls:
type Person interface {
job()
growUp()
}
type Student struct { age int }
func (p Student) job() { fmt.Println("I am a student.") }
func (p *Student) growUp() { p.age++ }
type Programmer struct { age int }
func (p Programmer) job() { fmt.Println("I am a programmer.") }
func (p Programmer) growUp() { p.age += 10 }
func whatJob(p Person) { p.job() }
func growUp(p Person) { p.growUp() }Running the program prints the appropriate messages and updated ages, illustrating runtime polymorphism.
9. Comparison with C++ Interfaces
The article contrasts Go's non‑intrusive interface implementation with C++'s abstract‑class (pure virtual) approach. It notes that Go's itab and function pointers are generated at runtime, whereas C++ v‑tables are created at compile time.
10. References
A list of reference links and author information is provided at the end of the article.
Didi Tech
Official Didi technology account
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.