SoFunction
Updated on 2025-03-05

Detailed explanation of the use of Golang's pointer and interface

Pointers and interfaces

The golang type system is actually very interesting. The interesting thing is that the type system looks equal on the surface, but in fact it has to be divided into ordinary types and interfaces. Normal types also contain so-called reference types, such as slice and map. Although they are reference types as interface, their behavior is closer to ordinary built-in types and custom types, so only unique interfaces will be classified separately.

So what do we divide golang into two categories? It is actually very simple. It depends on whether the type can be determined during the compilation period and whether the type method called can be determined during the compilation period.

If you think the above explanation is too abstract, you can first look at the following example:

package main
 
import "fmt"
 
func main(){
    m := make(map[int]int)
    m[1] = 1 * 2
    m[2] = 2 * 2
    (m)
    m2 := make(map[string]int)
    m2["python"] = 1
    m2["golang"] = 2
    (m2)
}

First, let’s look at non-interface reference types. m and m2 are obviously two different types, but in fact they are the same at the bottom. If you don’t believe it, let’s use the objdump tool to check:

go tool objdump -s 'main\.main' a
 
TEXT (SB) /tmp/
  :6  CALL runtime.makemap_small(SB)     # m := make(map[int]int)
  ...
  :7  CALL runtime.mapassign_fast64(SB)  # m[1] = 1 * 2
  ...
  :8  CALL runtime.mapassign_fast64(SB)  # m[2] = 2 * 2
  ...
  ...
  :10 CALL runtime.makemap_small(SB)     # m2 := make(map[string]int)
  ...
  :11 CALL runtime.mapassign_faststr(SB) # m2["python"] = 1
  ...
  :12 CALL runtime.mapassign_faststr(SB) # m2["golang"] = 2

Some register operations and calls of irrelevant functions have been omitted, and the original text of the corresponding code is added. We can clearly see that despite the different types, the map calls methods are the same and have been determined at the compile time. What if it is a custom type?

package main
 
import "fmt"
 
type Person struct {
    name string
    age int
}
 
func (p *Person) sayHello() {
    ("Hello, I'm %v, %v year(s) old\n", , )
}
 
func main(){
    p := Person{
        name: "apocelipes",
        age: 100,
    }
    ()
}

This time we created a custom type with custom fields and methods, and then check it with objdump:

go tool objdump -s 'main\.main' b
 
TEXT (SB) /tmp/
  ...
  :19   CALL main.(*Person).sayHello(SB)
  ...

Creating objects with literals and initializing the assembly code of the call stack is not the point, the focus is on the CALL. We can see that the method of custom types is also determined during the compilation period.

Then, let’s look at the difference between interface:

package main
 
import "fmt"
 
type Worker interface {
    Work()
}
 
type Typist struct{}
func (*Typist)Work() {
    ("Typing...")
}
 
type Programer struct{}
func (*Programer)Work() {
    ("Programming...")
}
 
func main(){
    var w Worker = &Typist{}
    ()
    w = &Programer{}
    ()
}

Notice! Compiling this program requires the compiler to prevent optimization, otherwise the compiler will directly optimize the interface method search to a specific type of method call:

go build -gcflags "-N -l"
go tool objdump -S -s 'main\.main' c
 
TEXT (SB) /tmp/
  ...
  var w Worker = &Typist{}
    LEAQ (SB), AX
    MOVQ AX, 0x10(SP)
    MOVQ AX, 0x20(SP)
    LEAQ .*,(SB), CX
    MOVQ CX, 0x28(SP)
    MOVQ AX, 0x30(SP)
  ()
    MOVQ 0x28(SP), AX
    TESTB AL, 0(AX)
    MOVQ 0x18(AX), AX
    MOVQ 0x30(SP), CX
    MOVQ CX, 0(SP)
    CALL AX
  w = &Programer{}
    LEAQ (SB), AX
    MOVQ AX, 0x8(SP)
    MOVQ AX, 0x18(SP)
    LEAQ .*,(SB), CX
    MOVQ CX, 0x28(SP)
    MOVQ AX, 0x30(SP)
  ()
    MOVQ 0x28(SP), AX
    TESTB AL, 0(AX)
    MOVQ 0x18(AX), AX
    MOVQ 0x30(SP), CX
    MOVQ CX, 0(SP)
    CALL AX
  ...

This time we can see that the method calling the interface will search in runtime, and then the address found by CALL is not to find the corresponding function during the compilation period like before. This is why interface is special: interface is a dynamically changing type.

The most obvious benefit of the types that can be dynamically changed is to give the program a high degree of flexibility, but flexibility comes at a cost, mainly in two aspects.

First, the performance cost. Dynamic method searches always cost several assembly instructions more than method calls that can be determined during the compilation period (mov and lea usually produce actual instructions), and the performance impact will occur after the number is accumulated. However, the good news is that the compiler usually optimizes our code. For example, if we do not turn off the compiler optimization, the compiler will complete the search of the method for us during compilation, and there will be no dynamic search content in the actual produced code. However, the bad news is that this optimization requires the compiler to determine the actual type of interface reference data during the compilation period, considering the following code:

type Worker interface {
    Work()
}
 
for _, v := workers {
    ()
}

Because as long as the Worker interface type is implemented, one can stuff its own instance into the workers slice, the compiler cannot determine the type of data referenced by v, so there is no way to discuss optimization.

Another cost, to be precise, should be called a trap, which is the topic we will discuss next.

golang pointer

Pointers are also a very valuable topic, especially the various black technologies of pointers in reflect and runtime packages. But let's relax, today we only need to understand the automatic dereference of pointers.

Let's change the code in one line:

p := &Person{
    name: "apocelipes",
    age: 100,
}

p is now a pointer, and the rest of the code does not require any changes, and the program can still be compiled and executed normally. The corresponding assembly is like this style (of course, optimization must be turned off):

()
    MOVQ AX, 0(SP)
    CALL main.(*Person).sayHello(SB)

Compare the non-pointer version:

()
    LEAQ 0x8(SP), AX
    MOVQ AX, 0(SP)
    CALL main.(*Person).sayHello(SB)

Rather than automatically dereference the pointer, it is better to say that the non-pointer version first finds the actual address of the object, and then passes this address as the method's receiver calls the method. It's nothing strange, either, because our method is pointer receiver:P.

If you change the receiver to a value type receiver:

()
    TESTB AL, 0(AX)
    MOVQ 0x40(SP), AX
    MOVQ 0x48(SP), CX
    MOVQ 0x50(SP), DX
    MOVQ AX, 0x28(SP)
    MOVQ CX, 0x30(SP)
    MOVQ DX, 0x38(SP)
    MOVQ AX, 0(SP)
    MOVQ CX, 0x8(SP)
    MOVQ DX, 0x10(SP)
    CALL (SB)

As a comparison:

()
    MOVQ AX, 0(SP)
    MOVQ $0xa, 0x8(SP)
    MOVQ $0x64, 0x10(SP)
    CALL (SB)

At this time, golang checks the pointer first and then dereferences. At the same time, it should be noted that the method call here has been determined during the compilation period.

Pointer to interface

After laying the groundwork for so long, it’s finally time to get to the point. However, there is still a little preparatory knowledge before this:

A pointer type denotes the set of all pointers to variables of a given type, called the base type of the pointer. --- go language spec

In other words, as long as the type that can get the address, there will be a corresponding pointer type. It is more coincidental that the reference type in golang can get the address, including the interface.

With these previews, now we can take a look at our rapper program:

package main
 
import "fmt"
 
type Rapper interface {
    Rap() string
}
 
type Dean struct {}
 
func (_ Dean) Rap() string {
    return "Im a rapper"
}
 
func doRap(p *Rapper) {
    (())
}
 
func main(){
    i := new(Rapper)
    *i = Dean{}
    (())
    doRap(i)
}

The question is, can the young Dean realize his dream of rap?

Unfortunately, the compiler gave an objection:

# command-line-arguments
./:16:18: undefined (type *Rapper is pointer to interface, not interface)
./:22:18: undefined (type *Rapper is pointer to interface, not interface)

Maybe you are not unfamiliar with the error of type *XXX is pointer to interface, not interface. You have made the error of pointing to interface with a pointer. After a search, you found a tutorial, or a blog, or any information from any place. They will tell you that you should not use pointers to point to interface. The interface itself is a reference type and there is no need to use pointers to reference it.

In fact, they are only half right. In fact, as long as i and p are changed to interface types, they can be compiled and run normally. The half that is not right is that pointers can point to interfaces, or use interface methods, but you have to take some detours (of course, referring to interfaces with pointers is usually an unnecessary move, so there is nothing wrong with following experience):

func doRap(p *Rapper) {
    ((*p).Rap())
}
 
func main(){
    i := new(Rapper)
    *i = Dean{}
    ((*i).Rap())
    doRap(i)
}

go run  
 
Im a rapper
Im a rapper

A magical scene occurred, and the program not only reported no errors but also ran normally. But what is the difference between this and golang's automatic dereference of pointers? It looks the same, but the first solution will be reported
Can't find the Rap method?

For the sake of easy observation, we extract the call statements separately and then view the unoptimized assembly code:

s := (*p).Rap()
  0x498ee1              488b842488000000        MOVQ 0x88(SP), AX
  0x498ee9              8400                    TESTB AL, 0(AX)
  0x498eeb              488b08                  MOVQ 0(AX), CX
  0x498eee              8401                    TESTB AL, 0(CX)
  0x498ef0              488b4008                MOVQ 0x8(AX), AX
  0x498ef4              488b4918                MOVQ 0x18(CX), CX
  0x498ef8              48890424                MOVQ AX, 0(SP)
  0x498efc              ffd1                    CALL CX

Putting aside the manual dereference, the last 6 lines are actually the same as using the interface for dynamic query directly. The real problem lies in automatic dereference:

()
    TESTB AL, 0(AX)
    MOVQ 0x40(SP), AX
    MOVQ 0x48(SP), CX
    MOVQ 0x50(SP), DX
    MOVQ AX, 0x28(SP)
    MOVQ CX, 0x30(SP)
    MOVQ DX, 0x38(SP)
    MOVQ AX, 0(SP)
    MOVQ CX, 0x8(SP)
    MOVQ DX, 0x10(SP)
    CALL (SB)

The difference is that on this CALL, the CALL when automatically dereferenced actually treats the content pointed to by the pointer as an ordinary type, so it will statically search the method and call it. When the content pointed to is an interface, the compiler will go to the interface's own data structure to find out if there is a Rap method. The answer is obviously no, so it exploded with an undefined error.

So what is the real appearance of interface? Let’s take a look at the implementation of go1.15.2:

// src/runtime/
// Because there is no empty interface here, only implementations containing data interfaces have been excerptedtype iface struct {
    tab  *itab
    data 
}
 
// src/runtime/
type itab struct {
    inter *interfacetype
    _type *_type
    hash  uint32 // copy of _type.hash. Used for type switches.
    _     [4]byte
    fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
 
// src/runtime/
type imethod struct {
    name nameOff
    ityp typeOff
}
 
type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod // All methods included in the type}
 
// src/runtime/
type _type struct {
    size       uintptr
    ptrdata    uintptr // size of memory prefix holding all pointers
    hash       uint32
    tflag      tflag
    align      uint8
    fieldAlign uint8
    kind       uint8
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    equal func(, ) bool
    // gcdata stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, gcdata is a GC program.
    // Otherwise it is a ptrmask bitmap. See  for details.
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

Types not defined are typing alias for various integer types. The interface is actually a struct that stores type information and actual data. After automatic dereference, the compiler directly views the memory content (see assembly). At this time, what you see is actually the ordinary type of ife, so static searching for a non-existent method fails. And why can manually dereferenced code run? Because after we manually dereference, the compiler can deduce that the actual type is interface, at this time, the compiler will naturally use the method of processing the interface to process it instead of directly addressing the things in the memory and stuffing them into the register.

Summarize

Actually, there is nothing to summarize. There are only two points to remember. One is that the interface has its own corresponding entity data structure, and the other is to try not to point to the interface with a pointer, because golang's automatic dereference of pointers will bring traps.

If you are interested in the implementation of interface, here is a reflection+violent exhaustive implementationBeggar's version

This is the end of this article about the detailed explanation of the use of Golang's pointers and interfaces. For more related Golang pointers, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!