Análisis del principio de implementación de la interfaz de Golang

Análisis de interfaz


Este artículo se basa en el análisis del código fuente go1.12.12, el código se ejecuta y se depura en la máquina amd64

1. Mecanografía de patos

1.1 ¿Qué es el tipeo de patos?

1

(Fuente: Enciclopedia Baidu)

¿El gran pato amarillo de la imagen es un pato? Desde un punto de vista tradicional, el gran pato amarillo de la imagen no es un pato, porque no puede gritar ni correr, y ni siquiera está vivo.

Primer vistazo a la definición de tipo de pato, tomada de Wikipedia

Si camina como un pato y grazna como un pato, entonces debe ser un pato.

Si algo camina como un pato y grazna como un pato, debe ser un pato

Entonces, desde Duck Typingel punto de vista, el gran pato amarillo en la imagen es un pato.

Duck typing, un estilo de inferencia de tipos en programación que describe el comportamiento externo de las cosas en lugar de su estructura interna.

1.2 Duck escribiendo en el idioma Go

El lenguaje Go se implementa a través de interfaces Duck Typing. A diferencia de otros lenguajes dinámicos, en los que las discrepancias de tipos solo se pueden verificar en tiempo de ejecución y, a diferencia de la mayoría de los lenguajes estáticos, en los que debe declarar explícitamente qué interfaz implementar, las interfaces de lenguaje Go son únicas en el sentido de que son隐式实现

2. Resumen

2.1 Tipo de interfaz

Una interfaz es un tipo de interfaz 抽象类型que no expone el diseño o la estructura interna de los datos que contiene y, por supuesto, no existe una operación básica de los datos y solo se proporcionan algunos métodos. Cuando obtiene una variable de tipo interfaz, no tiene forma de saber qué es, pero puede saber qué puede hacer, o más precisamente, qué métodos proporciona.

2.2 Definición de interfaz

El lenguaje Go proporciona interfacela palabra clave y la interfaz solo puede definir los métodos que deben implementarse y no puede contener ninguna variable.

type 接口类型名 interface{
    
    
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2}

Por ejemplo, io.Writerel tipo es en realidad el tipo de interfaz

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

Se pueden anidar nuevas interfaces entre interfaces, como io.ReadWriter:

type Reader interface {
    
    
    Read(p []byte) (n int, err error)
}

type ReadWriter interface{
    
    
    Reader
    Writer
}

Una interfaz que no contiene métodos se denomina tipo de interfaz vacía.

interface{
    
    }

2.3 Implementar la interfaz

Un tipo concreto implementa una interfaz si implementa todos los métodos requeridos por la interfaz. Cuando una expresión implementa una interfaz, la expresión solo se puede copiar a la interfaz

En el siguiente ejemplo, defina una Runnerinterfaz que contenga solo un run()método y Personla estructura implemente Run()el método , luego se implementa Runnerla interfaz

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

Además, debido a que el tipo de interfaz vacía es una interfaz que no define ningún método, todos los tipos implementan la interfaz vacía, lo que significa que se puede asignar cualquier valor al tipo de interfaz vacía.

2.4 Interfaces y punteros

Cuando una interfaz define un conjunto de métodos, no limita los destinatarios de la implementación, por lo que hay dos métodos de implementación, uno es un receptor de puntero y el otro es un receptor de valor.

No pueden existir dos implementaciones del mismo método al mismo tiempo

Agregue un Say()método y el tipo de estructura Person usa el puntero receptor para implementar el método Say()

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

Al inicializar variables de interfaz, puede usar estructuras o punteros de estructura

var r Runner
r = &Person{
    
    Name: "sch_chh"}
r = Person{
    
    Nmae: "sch_chh"}

Debido a que tanto el tipo de receptor que implementa la interfaz como el tipo cuando se inicializa la interfaz tienen dos dimensiones, se generan cuatro codificaciones diferentes

  • | receptor de valor| receptor de puntero—
    |—|
    —inicialización de valor| √ | ×
    inicialización de puntero| √ | √

×Indica que la compilación falló

Las siguientes dos situaciones pueden entenderse bien a través de la compilación:

  • Tanto el receptor del método como el tipo de inicializador son valores de estructura
  • El receptor del método y el tipo de inicialización son punteros de estructura.

Primero, echemos un vistazo a la situación que puede pasar la compilación, es decir, el receptor del método es una estructura y la variable inicializada es un tipo de puntero.

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: "sch_chh"}
    r.Run()
    r.Say()
}

En el código anterior, Personel puntero de estructura se puede llamar directamente Run, Sayporque como puntero de estructura, la estructura subyacente se puede obtener implícitamente, y luego se llama al método correspondiente a través de la estructura

Si se elimina la referencia, la inicialización de la variable utiliza el tipo de estructura

r = Person{
    
    Name: "sch_chh"}

Indicará que la compilación falla

./pointer.go:24:4: cannot use Person literal (type Person) as type Runner in assignment:
        Person does not implement Runner (Say method has pointer receiver)

Entonces, ¿por qué falla la compilación? En primer lugar, en el lenguaje Go, el paso de parámetros es值传递

Cuando la variable en el código &Person{}es , los parámetros se copiarán durante la llamada al método y Personse creará un nuevo puntero de estructura, que apunta a una determinada estructura, por lo que el compilador desreferenciará implícitamente la variable para obtener el puntero a la estructura. para completar la llamada al método

2

Cuando la variable en el código Person{}es , los parámetros se copiarán durante la llamada al método, es decir, yRun() aceptarán una nueva variable. Si el receptor del método es , el compilador no puede encontrar un puntero único basado en la estructura, por lo que el compilador informará un error.Say()Person{}*Person

3

Nota: para una variable de tipo T específico, también es legal llamar directamente al método *T, porque el compilador completará implícitamente la operación de búsqueda de direcciones por usted, pero esto es solo un azúcar sintáctico.

2.5 cero no cero

Mire otro ejemplo, aún la interfaz Runner y la estructura Person, preste atención al cuerpo de la función main(), primero declare una variable de interfaz r, imprima si es nulo, luego defina un tipo *Person p, imprima si p es nulo , y finalmente Asigne p a r, e imprima si r es nulo en este momento

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
    fmt.Println("r:", r == nil)

    var p *Person
    fmt.Println("p:", p == nil)

    r = p 
    fmt.Println("r:", r == nil)
}

¿Cuál es la salida?

r: true or false
p: true or false
r: true or false

La salida real es:

r: true
p: true
r: false

Es comprensible que las dos primeras salidas r sean nulas y p sean nulas, porque el valor cero del tipo de interfaz y el tipo de puntero son nulas, entonces cuando p se asigna a r, ¿r no es nula? De hecho, existe un concepto de valor de interfaz

2.6 Valores de la interfaz

Hablando conceptualmente, un valor de un tipo de interfaz (abreviado como valor de interfaz) en realidad tiene dos partes: 具体类型y denominan 该类型的值la suma de la interfaz , por lo que si y solo si el tipo dinámico y el valor dinámico de la interfaz son ambos. nil, el valor de la interfaz es nulo动态类型动态值

Volviendo al ejemplo de 2.5, cuando p se asigna a la interfaz r, la estructura real de r se muestra en la figura

4

Para verificar si este es realmente el caso, agregue una línea de código al final del cuerpo de la función principal

fmt.Printf("r type: %T, data: %v\n", r, r)

resultado de la operación

r type: *main.Person, data: <nil>

Puedes ver que el valor dinámico es de hecho cero

Ahora que conocemos el concepto de valor de interfaz, ¿cuál es la implementación específica de la interfaz subyacente?

3. Principio de implementación

El tipo de interfaz en el lenguaje Go se dividirá 是否包含一组方法en dos implementaciones diferentes, a saber, ifaceuna estructura que contiene un conjunto de métodos y efaceuna estructura sin ningún método.

3.1 cara

La capa inferior de iface es una estructura, que se define de la siguiente manera:

//runtime/runtime2.go
type iface struct {
    
    
        tab  *itab
        data unsafe.Pointer
}

Hay dos punteros dentro de iface, uno es el puntero de estructura itab y el otro es el puntero a los datos.

El tipo unsafe.Pointer es un tipo especial de puntero que puede almacenar la dirección de cualquier variable (similar a void* en C)

//runtime/runtime2.go
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.
}

itab se usa para indicar la relación entre el tipo específico y el tipo de interfaz, donde interestá la información de definición del tipo de interfaz, _typees la información de tipo específico hashy es una copia de _type.hash.Durante la conversión de tipos, juzgue rápidamente si el tipo de destino es consistente con el tipo en la interfaz fun. Lista de direcciones de métodos. Aunque fun es una matriz con una longitud fija de 1, en realidad es una matriz flexible. La cantidad de elementos almacenados es incierta. Los métodos múltiples se ordenan en el diccionario

//runtime/type.go
type interfacetype struct {
    
    
        typ     _type
        pkgpath name
        mhdr    []imethod
}
```go
interfacetype是描述接口定义的信息,`_type`:接口的类型信息,`pkgpath`是定义接口的包名;,`mhdr`是接口中定义的函数表,按字典序排序

> 假设接口有ni个方法,实现接口的结构体有nt个方法,那么itab函数表生成时间复杂为O(ni*nt),如果接口方法列表和结构体方法列表有序,那么函数表生成时间复杂度为O(ni+nt)

```go
//runtime/type.go
type _type struct {
    
    
        size       uintptr
        ptrdata    uintptr // size of memory prefix holding all pointers
        hash       uint32
        tflag      tflag
        align      uint8
        fieldalign uint8
        kind       uint8
        alg        *typeAlg
        // 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 mbitmap.go for details.
        gcdata    *byte
        str       nameOff
        ptrToThis typeOff
}

_type es una descripción común para todos los tipos. sizeEs el tamaño del tipo, que hashes el valor hash del tipo, tflagson las etiquetas del tipo, relacionadas con la reflexión aligny fieldalignrelacionadas con la alineación de la memoria, y kindes el número de tipo.La definición específica se encuentra en runtime/typekind .go, que gcdataes información relacionada con gc

El diagrama de estructura de toda la cara es el siguiente:

5

3.2 cara

En comparación con iface, la estructura de eface es relativamente simple

//runtime/runtime2.go
type eface struct {
    
    
        _type *_type
        data  unsafe.Pointer
}

También hay dos punteros dentro de eface, un puntero a la estructura de tipo de información de tipo específico y un puntero a datos

6

3.3 Conversión de tipo concreto a tipo interfaz

Hasta ahora, ¿qué es una interfaz, la estructura subyacente de la interfaz y cómo se realiza la conversión cuando el tipo específico se asigna al tipo de interfaz? Veamos el ejemplo en la implementación de la interfaz.

  1 package main
  2 
  3 import "fmt"
  4 
  5 type Runner interface {
    
    
  6     Run()
  7 }
  8 
  9 type Person struct {
    
    
 10     Name string
 11 }
 12 
 13 func (p Person) Run() {
    
    
 14     fmt.Printf("%s is running\n", p.Name)
 15 }
 16 
 17 func main() {
    
    
 18     var r Runner
 19     r = Person{
    
    Name: "song_chh"}
 20     r.Run()
 21 }

Genere código ensamblador a través de las herramientas proporcionadas por Go

go tool compile -S interface.go

Solo intercepta el código relacionado con la línea 19

0x001d 00029 (interface.go:19)  PCDATA  $2, $0
0x001d 00029 (interface.go:19)  PCDATA  $0, $1
0x001d 00029 (interface.go:19)  XORPS   X0, X0
0x0020 00032 (interface.go:19)  MOVUPS  X0, ""..autotmp_1+32(SP)
0x0025 00037 (interface.go:19)  PCDATA  $2, $1
0x0025 00037 (interface.go:19)  LEAQ    go.string."song_chh"(SB), AX
0x002c 00044 (interface.go:19)  PCDATA  $2, $0
0x002c 00044 (interface.go:19)  MOVQ    AX, ""..autotmp_1+32(SP)
0x0031 00049 (interface.go:19)  MOVQ    $8, ""..autotmp_1+40(SP)
0x003a 00058 (interface.go:19)  PCDATA  $2, $1
0x003a 00058 (interface.go:19)  LEAQ    go.itab."".Person,"".Runner(SB), AX
0x0041 00065 (interface.go:19)  PCDATA  $2, $0
0x0041 00065 (interface.go:19)  MOVQ    AX, (SP)
0x0045 00069 (interface.go:19)  PCDATA  $2, $1
0x0045 00069 (interface.go:19)  PCDATA  $0, $0
0x0045 00069 (interface.go:19)  LEAQ    ""..autotmp_1+32(SP), AX
0x004a 00074 (interface.go:19)  PCDATA  $2, $0
0x004a 00074 (interface.go:19)  MOVQ    AX, 8(SP)
0x004f 00079 (interface.go:19)  CALL    runtime.convT2I(SB)
0x0054 00084 (interface.go:19)  MOVQ    16(SP), AX
0x0059 00089 (interface.go:19)  PCDATA  $2, $2
0x0059 00089 (interface.go:19)  MOVQ    24(SP), CX

Se puede ver que el compilador llama a la función de conversión después de construir itab runtime.convT2I(SB), vea la implementación de la función

//runtime/iface.go
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
    
    
        t := tab._type
        if raceenabled {
    
    
                raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
        }
        if msanenabled {
    
    
                msanread(elem, t.size)
        }
        x := mallocgc(t.size, t, true)
        typedmemmove(t, x, elem)
        i.tab = tab
        i.data = x
        return
}

mallocgcPrimero, llame para solicitar un espacio de memoria de acuerdo con el tamaño del tipo elem, copie el contenido del puntero al nuevo espacio, asigne la pestaña a la pestaña de iface y asigne el nuevo puntero de memoria a los datos de iface. , para que se cree un iface

Cambie ligeramente el código de muestra para asignar una variable de tipo de puntero de estructura a una variable de interfaz

 19     r = &Person{
    
    Name: "song_chh"}

Genere código ensamblador nuevamente a través de la herramienta

go tool compile -S interface.go

Ver el siguiente código ensamblador

0x001d 00029 (interface.go:19)  PCDATA  $2, $1
0x001d 00029 (interface.go:19)  PCDATA  $0, $0
0x001d 00029 (interface.go:19)  LEAQ    type."".Person(SB), AX
0x0024 00036 (interface.go:19)  PCDATA  $2, $0
0x0024 00036 (interface.go:19)  MOVQ    AX, (SP)
0x0028 00040 (interface.go:19)  CALL    runtime.newobject(SB)
0x002d 00045 (interface.go:19)  PCDATA  $2, $2
0x002d 00045 (interface.go:19)  MOVQ    8(SP), DI
0x0032 00050 (interface.go:19)  MOVQ    $8, 8(DI)
0x003a 00058 (interface.go:19)  PCDATA  $2, $-2
0x003a 00058 (interface.go:19)  PCDATA  $0, $-2
0x003a 00058 (interface.go:19)  CMPL    runtime.writeBarrier(SB), $0
0x0041 00065 (interface.go:19)  JNE     105
0x0043 00067 (interface.go:19)  LEAQ    go.string."song_chh"(SB), AX
0x004a 00074 (interface.go:19)  MOVQ    AX, (DI)

Primero, el compilador obtiene Personel puntero del tipo de estructura, llama runtime.newobject()a la función como parámetro y también verifica la definición de la función en el código fuente.

// runtime/malloc.go

// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
    
    
        return mallocgc(typ.size, typ, true)
}

newobject toma *Person como parámetro de entrada, crea un nuevo Personpuntero de estructura y establece sus variables, y luego el compilador genera iface

Además convT2Ide funciones, de hecho runtime/runtime.go, hay muchas definiciones de funciones de conversión en el archivo.

// Non-empty-interface to non-empty-interface conversion.
func convI2I(typ *byte, elem any) (ret any)

// Specialized type-to-interface conversion.
// These return only a data pointer.
func convT16(val any) unsafe.Pointer     // val must be uint16-like (same size and alignment as a uint16)
func convT32(val any) unsafe.Pointer     // val must be uint32-like (same size and alignment as a uint32)
func convT64(val any) unsafe.Pointer     // val must be uint64-like (same size and alignment as a uint64 and contains no pointers)
func convTstring(val any) unsafe.Pointer // val must be a string
func convTslice(val any) unsafe.Pointer  // val must be a slice

// Type to empty-interface conversion.
func convT2E(typ *byte, elem *any) (ret any)
func convT2Enoptr(typ *byte, elem *any) (ret any)

// Type to non-empty-interface conversion.   
func convT2I(tab *byte, elem *any) (ret any)        //for the general case
func convT2Inoptr(tab *byte, elem *any) (ret any)   //for structs that do not contain pointers

convT2InoptrSe utiliza para la conversión sin puntero dentro de la estructura. noptr puede entenderse como sin puntero. El proceso de conversión es similar convT2Ial
deconvT16convT32convT64convTstringconvTsliceconvT64

//runtime/iface.go
func convT64(val uint64) (x unsafe.Pointer) {
    
    
        if val == 0 {
    
    
                x = unsafe.Pointer(&zeroVal[0])
        } else {
    
    
                x = mallocgc(8, uint64Type, false)
                *(*uint64)(x) = val
        }
        return
}

En comparación con convT2una serie de funciones, carece de llamadas typedmemmovea memmovefunciones y reduce la copia de memoria. Además, si el valor es un valor cero de este tipo, no llamará mallocgcpara solicitar una nueva memoria y devolverá directamente el zeroVal[0]puntero puntiagudo

Veamos la función de conversión de interfaz vacíaconvT2E

func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
    
    
        if raceenabled {
    
    
                raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
        }
        if msanenabled {
    
    
                msanread(elem, t.size)
        }
        x := mallocgc(t.size, t, true)
        // TODO: We allocate a zeroed object only to overwrite it with actual data.
        // Figure out how to avoid zeroing. Also below in convT2Eslice, convT2I, convT2Islice.
        typedmemmove(t, x, elem)
        e._type = t
        e.data = x
        return
}

convT2EconvT2ISimilar a , *_typetambién lo genera el compilador cuando se convierte a eface, y se llama como un parámetro de entradaconvT2E

3.4 Afirmaciones

El contenido de la sección anterior presenta principalmente cómo convertir un tipo concreto en un tipo de interfaz, entonces, ¿cómo convertir un tipo concreto en un tipo de interfaz? El lenguaje Go proporciona dos métodos, a saber 类型断言y类型分支

tipo aserción

Hay dos formas de escribir aserciones de tipo

    v := x.(T)
v, ok := x.(T)
  • x: es una expresión del tipo de interfaz
  • T: es un tipo conocido

Preste atención a la primera forma de escribir, si la afirmación de tipo falla, se activará painc

interruptor de tipo

switch x := x.(type) {
    
     /* ... */}

Ejemplo de uso

switch i.(type) {
    
    
case string:
    fmt.Println("i'm a string")
case int:
    fmt.Println("i'm a int")
default:
    fmt.Println("unknown")
} 

4. Referencias

【1】 Prensa de la industria de maquinaria "Ir al lenguaje de programación"

[2] "Análisis de la capa inferior de la interfaz en golang"

[3] "Sobre los principios de la implementación del lenguaje Go"

[4] "Descifrado profundo Go Language 10 preguntas sobre la interfaz"

Supongo que te gusta

Origin blog.csdn.net/zhw21w/article/details/129488201
Recomendado
Clasificación