3
votes

I recently encountered empty interfaces while using the Load() method of Atomic.Value . I was experimenting with empty interfaces type assertion a bit - https://play.golang.org/p/CLyY2y9-2VF

This piqued my interest, and I decided to take a peek behind the curtains to see what actions does a compiler take so that the code doesn't panic in case of trying to read the concrete value on a nil interface {} (e.g., when you call Load.(type) when Store hasn't been called yet).

I could see that in the unsafe version, compiler had this assembly instruction that cause the panic : call runtime.panicdottypeE(SB)

The panic instruction is obviously not present in the safe version. Can someone please explain this in more details on what compiler is doing when we capture return value with ok (and perhaps point me to the corresponding assembly instructions in the godbolt link)?

Here are the godbolt compiler links for unsafe version [1] and safe version [2].

[1] https://godbolt.org/z/76onvj

[2] https://godbolt.org/z/e8aoqe

2

2 Answers

0
votes

Empty interface type (called eface in runtime package) is 2 pointers, first one to underlying type (e.g. bool type, int type, YourStruct type, ...) second one is a pointer to data (IIRC in some cases the data itself).

Unsafe version:

call "".returnEmptyInterface(SB)   Call the function
pcdata $0, $1                      pcdata is not a real instruction, ignore 
movq 8(SP), AX                     AX <- pointer to data 
movq    (SP), CX                   CX <- pointer to type 
pcdata $0, $2 
leaq type.bool(SB), DX             DX <- pointer to bool type 
cmpq CX, DX                        Compare CX and DX 
jne main_pc156                     If they are not equal jump to main_pc156

In main_pc156 compiler will call runtime.panicdottypeE that will basically panic. Source code from runtime/iface.go (can find it in your $GOPATH/src):

func panicdottypeE(have, want, iface *_type) {
    panic(&TypeAssertionError{iface, have, want, ""})
}

Safe version:

pcdata  $0, $0
pcdata  $1, $0
call    "".returnEmptyInterface(SB)    Call the function
pcdata  $0, $1
movq    8(SP), AX                      AX <- pointer to data
movq    (SP), CX                       CX <- pointer to type
main_pc47:
pcdata  $0, $2
leaq    type.bool(SB), DX              DX <- pointer to bool type
cmpq    CX, DX                         Compare CX and DX
jne     main_pc186                     If not equal jump main_pc186
pcdata  $0, $3
movblzx (AX), AX                       AX <- dereference AX
main_pc62:

and in main_pc186

pcdata  $0, $3
xorl    AX, AX                        AX <- 0
jmp     main_pc62                     Jump back at the end of previous block

Here AX corresponds to x in the code but what corresponds to ok in code? Nothing! If you check code of println you see:

cmpq    CX, DX                        Compare CX and DX
seteq   AL                            AL <- 1 if CX equal to DX otherwise 0

So compiler decided to compare them again when printing.

0
votes

The quick summary: they do exactly the same thing, it's just that the case for ok == false is different.

The two bits have the following in common:

        pcdata  $0, $0
        pcdata  $1, $0
        call    "".returnEmptyInterface(SB)
        pcdata  $0, $1
        movq    8(SP), AX
        movq    (SP), CX
        pcdata  $0, $2
        leaq    type.bool(SB), DX
        cmpq    CX, DX
        jne     main_pc156   <==== jump
        pcdata  $0, $3
        movblzx (AX), AX

The code found in main_pc156 is the part we care about. As you noticed, for the single-value type assertion, this is:

main_pc156:
        movq    DX, 8(SP)
        pcdata  $0, $1
        leaq    type.interface {}(SB), AX
        pcdata  $0, $0
        movq    AX, 16(SP)
        call    runtime.panicdottypeE(SB)
        xchgl   AX, AX

There's no escaping this, once we jump to main_pc156, we panic.

On the other hand, the code for the two-value type assertion is:

main_pc186:
        pcdata  $0, $3
        xorl    AX, AX
        jmp     main_pc62

This is massively different from the previous case, and takes us right back to the end of the first bit of code, resuming execution.