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?
(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 Typing
el 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 interface
la 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.Writer
el 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 Runner
interfaz que contenga solo un run()
método y Person
la estructura implemente Run()
el método , luego se implementa Runner
la 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, Person
el puntero de estructura se puede llamar directamente Run
, Say
porque 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 Person
se 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
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
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
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, iface
una estructura que contiene un conjunto de métodos y eface
una 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 inter
está la información de definición del tipo de interfaz, _type
es la información de tipo específico hash
y 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. size
Es el tamaño del tipo, que hash
es el valor hash del tipo, tflag
son las etiquetas del tipo, relacionadas con la reflexión align
y fieldalign
relacionadas con la alineación de la memoria, y kind
es el número de tipo.La definición específica se encuentra en runtime/typekind .go, que gcdata
es información relacionada con gc
El diagrama de estructura de toda la cara es el siguiente:
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
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
}
mallocgc
Primero, 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 Person
el 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 Person
puntero de estructura y establece sus variables, y luego el compilador genera iface
Además convT2I
de 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
convT2Inoptr
Se utiliza para la conversión sin puntero dentro de la estructura. noptr puede entenderse como sin puntero. El proceso de conversión es similar convT2I
al
deconvT16
convT32
convT64
convTstring
convTslice
convT64
//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 convT2
una serie de funciones, carece de llamadas typedmemmove
a memmove
funciones y reduce la copia de memoria. Además, si el valor es un valor cero de este tipo, no llamará mallocgc
para 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
}
convT2E
convT2I
Similar a , *_type
tambié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"