Maison  >  Article  >  développement back-end  >  Une analyse approfondie de l'interface de Golang

Une analyse approfondie de l'interface de Golang

藏色散人
藏色散人avant
2021-01-28 17:36:451958parcourir

La colonne tutorielle suivante de golang vous donnera une analyse approfondie de l'interface de golang. J'espère qu'elle sera utile aux amis dans le besoin !

Une introduction à l'interface

Si gorountine et Channel sont les pierres angulaires qui prennent en charge le modèle de concurrence du langage Go, laissez le langage Go De nos jours, l'ère du clustering et du multicœur est devenue un paysage magnifique, les interfaces sont donc la pierre angulaire de toute la série de types du langage Go, permettant au langage Go d'atteindre des sommets sans précédent dans l'exploration de la philosophie de programmation de base. Le langage Go est une philosophie de programmation plutôt réformiste que réformiste. Ce n'est pas parce que le langage Go a un gorountine et un canal, mais plus important encore à cause du système de types du langage Go, et encore plus à cause de l'interface du langage Go. La philosophie de programmation du langage Go a tendance à être parfaite en raison des interfaces. C++ et Java utilisent des interfaces « intrusives », principalement parce que la classe d'implémentation doit déclarer explicitement qu'elle implémente une certaine interface. Cette méthode d'héritage d'interface obligatoire est une fonctionnalité qui a fait l'objet de doutes considérables dans le développement d'idées de programmation orientée objet. Le langage Go utilise une « interface non intrusive ». L'interface du langage Go a sa propre particularité : tant que la méthode publique de type T répond pleinement aux exigences de l'interface I, les objets de type T peuvent être utilisés là où l'interface I est requise. . Les méthodes dites publiques de type T répondent pleinement aux exigences de l'interface I, c'est-à-dire que le type T implémente un ensemble de membres spécifiés par l'interface I. Le nom scientifique de cette approche est le typage structurel, et certaines personnes le considèrent également comme une sorte de typage de canard statique.

Cette valeur implémente la méthode d'interface.

type Reader interface { 
 Read(p []byte) (n int, err os.Error) 
} 
 
// Writer 是包裹了基础 Write 方法的接口。 
type Writer interface { 
 Write(p []byte) (n int, err os.Error) 
} 
 
var r io.Reader 
r = os.Stdin 
r = bufio.NewReader(r) 
r = new(bytes.Buffer)

Une chose doit être claire. Quelle que soit la valeur r enregistrée, le type de r est toujours io.Reader Go est un type statique, et le type statique de r est io.Reader. Un exemple extrêmement important de type d'interface est l'interface vide interface{}, qui représente un ensemble de méthodes vide puisque toute valeur a zéro ou plusieurs méthodes, n'importe quelle valeur peut la satisfaire. Certaines personnes disent également que l'interface de Go est typée dynamiquement, mais c'est un malentendu. Elles sont typées statiquement : les variables de type interface ont toujours le même type statique et la valeur satisfait toujours l'interface vide, mais la valeur stockée dans la variable d'interface peut être modifiée au moment de l'exécution. Tout cela doit être traité avec prudence, car réflexion et interfaces sont étroitement liées.

2 Disposition de la mémoire du type d'interface

Une catégorie importante parmi les types est le type d'interface, qui exprime une collection A fixe de méthodes. Une variable d'interface peut stocker n'importe quelle valeur réelle (non-interface), à ​​condition que la valeur implémente la méthode de l'interface. L'interface se compose en fait de deux membres en mémoire, comme le montre la figure ci-dessous, l'onglet pointe vers la table virtuelle et les données pointent vers les données réellement référencées. La table virtuelle décrit les informations de type réelles et l'ensemble des méthodes requises par l'interface.

type Stringer interface { 
 String() string 
} 
 
type Binary uint64 
 
func (i Binary) String() string { 
 return strconv.FormatUint(i.Get(), 2) 
} 
 
func (i Binary) Get() uint64 { 
 return uint64(i) 
} 
 
func main() { 
 var b Binary = 32 
 s := Stringer(b) 
 fmt.Print(s.String()) 
}


Observez la structure de itable, d'abord quelques métadonnées décrivant les informations de type, puis une liste de pointeurs de fonction qui satisfont l'interface Stringger (notez qu'il s'agit de pas le type réel de pointeur de fonction binaire défini). Par conséquent, si nous effectuons un appel de fonction via l'interface, l'opération réelle est en fait s.tab->fun[0](s.data) . Est-ce similaire à la table virtuelle de C++ ? Mais ils sont fondamentalement différents. Regardons d'abord le C++. Il crée un ensemble de méthodes, c'est-à-dire une table virtuelle pour chaque classe. Lorsqu'une sous-classe remplace la fonction virtuelle de la classe parent, elle remplace le pointeur de fonction correspondant dans la table par la fonction implémentée par la sous-classe. Sinon, pointant vers l'implémentation de la classe parent, face à un héritage multiple, il y aura plusieurs pointeurs de table virtuelle dans la structure d'objet C++, et chaque pointeur de table virtuelle pointe vers une partie différente de l'ensemble de méthodes. Regardons l'implémentation de golang. Comme C++, golang crée également un ensemble de méthodes pour chaque type. La différence est que la table virtuelle de l'interface est spécialement générée au moment de l'exécution, tandis que la table virtuelle de C++ est générée au moment de la compilation ( Mais le polymorphisme affiché par la table de fonctions virtuelles c++ est déterminé au moment de l'exécution). Par exemple, lorsqu'une instruction comme s := Stringer(b) est rencontrée pour la première fois dans l'exemple, golang générera une table virtuelle de l'interface Stringer correspondant au binaire. tapez, et son cache sera. Alors pourquoi n'utilise-t-il pas C++ pour l'implémenter ? Cette disposition de la mémoire des objets C++ et Golang est liée.

首先c++的动态多态是以继承为基础的,在对象构造初始化的时首先会初始化父类,其次是子类,也就是说一个对象的内存布局是虚表,父类部分,子类部分(编译器不同可能会有差异),当一个父类指针指向子类时,会发生内存的截断,截断子类部分(内存地址偏移),但是此时子类的虚表中的函数指针实际上还是指向了自己的实现,所以此时的指针才会调用到子类的虚函数,如果不是虚函数,因为内存已经截断没有子类的非虚函数信息了,所以只能调用父类的了,这种继承关系让c++的虚表的初始化非常清晰,在一个对象初始化时先调用父类的构造此时虚表跟父类是一样的,接下来初始化子类,此时编译器就会去识别子类有没有覆盖父类的虚函数,如果有则虚表中相应的函数指针改成自己的虚函数实现指针。

那么go有什么不同呢,首先我们很清楚go是没有严格意义上的继承的,go的接口不存在继承关系,只要实现了接口定义的方法都可以成为接口类型,这给go的虚表初始化带来很大的麻烦,到底有多少类型实现了这个接口,一个类型到底实现了多少接口这让编译器很confused。举个例子,某个类型有m个方法,某接口有n个方法,则很容易知道这种判定的时间复杂度为O(mXn),不过可以使用预先排序的方式进行优化,实际的时间复杂度为O(m+n)这样看来其实还行那为什么要在运行时生成虚表呢,这不是会拖慢程序的运行速度吗,注意我们这里是某个类型,某个接口,是1对1的关系,如果有n个类型,n个接口呢,编译器难道要把之间所有的关系都理清吗?退一步说就算编译器任劳任怨把这事干了,可是你在写过程中你本来就不想实现那个接口,而你无意中给这个类型实现的方法中包含了某些接口的方法,你根本不需要这个接口(况且go的接口机制会导致很多这种无意义的接口实现),你欺负编译器就行了,这也太欺负人了吧。如果我们放到运行时呢,我们只要在需要接口的去分析一下类型是否实现了接口的所有方法就行了很简单的一件事。

三 空接口

接口类型的一个极端重要的例子是空接口:interface{} ,它表示空的方法集合,由于任何值都有零个或者多个方法,所以任何值都可以满足它。 注意,[]T不能直接赋值给[]interface{}

//t := []int{1, 2, 3, 4} wrong 
//var s []interface{} = t 
t := []int{1, 2, 3, 4} //right 
s := make([]interface{}, len(t)) 
for i, v := range t { 
 s[i] = v 
}
str, ok := value.(string) 
if ok { 
 fmt.Printf("string value is: %q\n", str) 
} else { 
 fmt.Printf("value is not a string\n") 
}

在Go语言中,我们可以使用type switch语句查询接口变量的真实数据类型,语法如下:

type Stringer interface { 
  String() string 
} 
 
var value interface{} // Value provided by caller. 
switch str := value.(type) { 
case string: 
  return str //type of str is string 
case Stringer: //type of str is Stringer 
  return str.String() 
}

也可以使用“comma, ok”的习惯用法来安全地测试值是否为一个字符串:

str, ok := value.(string) 
if ok { 
  fmt.Printf("string value is: %q\n", str) 
} else { 
  fmt.Printf("value is not a string\n") 
}

四 接口赋值

package main 
 
import ( 
"fmt" 
) 
 
type LesssAdder interface { 
  Less(b Integer) bool 
  Add(b Integer) 
} 
 
type Integer int 
 
func (a Integer) Less(b Integer) bool { 
  return a < b 
} 
 
func (a *Integer) Add(b Integer) { 
  *a += b 
} 
 
func main() { 
 
  var a Integer = 1 
  var b LesssAdder = &a 
  fmt.Println(b) 
 
  //var c LesssAdder = a 
  //Error:Integer does not implement LesssAdder  
  //(Add method has pointer receiver) 
}

go语言可以根据下面的函数:

func (a Integer) Less(b Integer) bool

自动生成一个新的Less()方法

func (a *Integer) Less(b Integer) bool

这样,类型*Integer就既存在Less()方法,也存在Add()方法,满足LessAdder接口。 而根据

func (a *Integer) Add(b Integer)

这个函数无法生成以下成员方法:

func(a Integer) Add(b Integer) { 
  (&a).Add(b) 
}

因为(&a).Add()改变的只是函数参数a,对外部实际要操作的对象并无影响(值传递),这不符合用户的预期。所以Go语言不会自动为其生成该函数。因此类型Integer只存在Less()方法,缺少Add()方法,不满足LessAddr接口。(可以这样去理解:指针类型的对象函数是可读可写的,非指针类型的对象函数是只读的)将一个接口赋值给另外一个接口 在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就等同的,可以相互赋值。 如果A接口的方法列表时接口B的方法列表的子集,那么接口B可以赋值给接口A,但是反过来是不行的,无法通过编译。

五 接口查询

接口查询是否成功,要在运行期才能够确定。他不像接口的赋值,编译器只需要通过静态类型检查即可判断赋值是否可行。

var file1 Writer = ...
if file5,ok := file1.(two.IStream);ok {
...
}

这个if语句检查file1接口指向的对象实例是否实现了two.IStream接口,如果实现了,则执行特定的代码。

在Go语言中,你可以询问它指向的对象是否是某个类型,比如,

var file1 Writer = ...
if file6,ok := file1.(*File);ok {
...
}

这个if语句判断file1接口指向的对象实例是否是*File类型,如果是则执行特定的代码。

slice := make([]int, 0)
slice = append(slice, 1, 2, 3)

var I interface{} = slice


if res, ok := I.([]int);ok {
  fmt.Println(res) //[1 2 3]
}

这个if语句判断接口I所指向的对象是否是[]int类型,如果是的话输出切片中的元素。

func Sort(array interface{}, traveser Traveser) error {

  if array == nil {
    return errors.New("nil pointer")
  }
  var length int //数组的长度
  switch array.(type) {
  case []int:
    length = len(array.([]int))
  case []string:
    length = len(array.([]string))
  case []float32:
    length = len(array.([]float32))

  default:
    return errors.New("error type")
  }

  if length == 0 {
    return errors.New("len is zero.")
  }

  traveser(array)

  return nil
}

通过使用.(type)方法可以利用switch来判断接口存储的类型。

Résumé : Demander si l'objet pointé par l'interface est d'un certain type peut être considéré comme un cas particulier de requête d'interface. Une interface est une abstraction des caractéristiques publiques d'un groupe de types, donc la différence entre interroger l'interface et interroger le type spécifique est comme la différence entre les deux questions suivantes :

Êtes-vous médecin ?

Oui.

Vous êtes Momomo

Oui

La première question interroge un groupe, qui est une interface de requête et la deuxième question interroge un ; groupe. Les deux questions ont atteint des individus spécifiques et sont des types de requêtes spécifiques.

De plus, la réflexion peut également être utilisée pour effectuer une requête de type, qui sera présentée en détail dans la réflexion.

Pour plus d'articles techniques liés au golang, veuillez visiter la colonne go language !

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer