Глубокое понимание интерфейса интерфейса Golang

Go

[TOC]

Глубокое понимание интерфейса интерфейса Golang

введение в интерфейс

Если горутины и каналы — два краеугольных камня параллелизма в Go, то интерфейсы — это ключ к типам данных в программировании на Go. В реальном программировании языка Go почти все структуры данных вращаются вокруг интерфейса, а интерфейс является ядром всех структур данных в языке Go.

Go не является типичным объектно-ориентированным языком, он синтаксически не поддерживает понятия классов и наследования.

Возможно ли иметь полиморфное поведение без наследования? Ответ — нет, язык Go представляет новый тип, интерфейс, который реализует концепцию «полиморфизма», аналогичную C++, хотя синтаксически он не эквивалентен полиморфизму C++, по крайней мере, в конечном итоге. полиморфная тень.

Хотя в языке Go нет концепции классов, поддерживаемые им типы данных могут определять соответствующие методы. По сути, так называемый(е) метод(ы) на самом деле является функцией, но по сравнению с обычными функциями, этот тип функции воздействует на определенный тип данных, поэтому в сигнатуре функции будет приемник (receiver), который указывает, что текущая определенная функция будет действовать на этот приемник.

Любой другой тип данных, поддерживаемый языком Go, за исключением типа интерфейса, может определять свой метод (не только структура поддерживает метод), но в реальных проектах методы в основном определяются в структуре. С этой точки зрения мы можем думать о структурах в Go как о легковесных «классах», которые не поддерживают поведение наследования.

Синтаксически интерфейс определяет один или группу методов, эти методы имеют только сигнатуры функций и не имеют конкретного кода реализации (вы думаете о виртуальных функциях в C++?). Говорят, что тип данных реализует интерфейс, если он реализует функции, называемые «методами», определенные в интерфейсе. Это наш общий объектно-ориентированный метод, ниже приведен простой пример.

   type MyInterface interface{
       Print()
   }
   
   func TestFunc(x MyInterface) {}
   type MyStruct struct {}
   func (me MyStruct) Print() {}
   
   func main() {
       var me MyStruct
       TestFunc(me)
   }

Why Interface

Зачем использовать интерфейс? В сообщении на Gopher China некоторые великие боги привели следующие причины:

написание общего алгоритма

скрытие деталей реализации

providing interception points

Ниже приведены три причины

написание общего алгоритма

Строго говоря, универсальное программирование в Golang не поддерживается. Использовать универсальное программирование на языках высокого уровня, таких как C++, очень просто, поэтому универсальное программирование всегда было наиболее критикуемым местом в Golang. Но с помощью интерфейса мы можем реализовать универсальное программирование, ниже приведен справочный пример.

    package sort

    // A type, typically a collection, that satisfies sort.Interface can be
    // sorted by the routines in this package.  The methods require that the
    // elements of the collection be enumerated by an integer index.
    type Interface interface {
        // Len is the number of elements in the collection.
        Len() int
        // Less reports whether the element with
        // index i should sort before the element with index j.
        Less(i, j int) bool
        // Swap swaps the elements with indexes i and j.
        Swap(i, j int)
    }
    
    ...
    
    // Sort sorts data.
    // It makes one call to data.Len to determine n, and O(n*log(n)) calls to
    // data.Less and data.Swap. The sort is not guaranteed to be stable.
    func Sort(data Interface) {
        // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached.
        n := data.Len()
        maxDepth := 0
        for i := n; i > 0; i >>= 1 {
            maxDepth++
        }
        maxDepth *= 2
        quickSort(data, 0, n, maxDepth)
    }
    

Формальным параметром функции Sort является интерфейс, который содержит три метода: Len(), Less(i, j int), Swap(i, j int). При его использовании, независимо от типа элемента массива (int, float, string...), пока мы реализуем эти три метода, мы можем использовать функцию Sort, таким образом реализуя «общее программирование».

Я также применил этот метод в проекте флеш-чата для сортировки сообщений.

Вот конкретный пример, код может все объяснить, его можно понять с первого взгляда:

   type Person struct {
   Name string
   Age  int
   }
   
   func (p Person) String() string {
       return fmt.Sprintf("%s: %d", p.Name, p.Age)
   }
   
   // ByAge implements sort.Interface for []Person based on
   // the Age field.
   type ByAge []Person //自定义
   
   func (a ByAge) Len() int           { return len(a) }
   func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
   
   func main() {
       people := []Person{
           {"Bob", 31},
           {"John", 42},
           {"Michael", 17},
           {"Jenny", 26},
       }
   
       fmt.Println(people)
       sort.Sort(ByAge(people))
       fmt.Println(people)
   }
   

скрытие деталей реализации

Скрыть конкретную реализацию, которую хорошо понимают. Например, если я проектирую функцию, которая возвращает вам интерфейс, то вы можете выполнять некоторые операции только через методы в интерфейсе, но внутренняя реализация совершенно неизвестна.

Например, наш часто используемый пакет контекста выглядит так: контекст был сначала предоставлен google, а теперь он включен в стандартную библиотеку и добавлен на основе исходного контекста: cancelCtx, timerCtx, valueCtx.

Мы только что говорили о контексте раньше, поэтому давайте рассмотрим его сейчас.

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        c := newCancelCtx(parent)
        propagateCancel(parent, &c)
        return &c, func() { c.cancel(true, Canceled) }
    }
    

Это указывает на то, что функция WithCancel возвращает интерфейс Context, но конкретной реализацией этого интерфейса является структура cancelCtx.

   
       // newCancelCtx returns an initialized cancelCtx.
       func newCancelCtx(parent Context) cancelCtx {
           return cancelCtx{
               Context: parent,
               done:    make(chan struct{}),
           }
       }
       
       // A cancelCtx can be canceled. When canceled, it also cancels any children
       // that implement canceler.
       type cancelCtx struct {
           Context     //注意一下这个地方
       
           done chan struct{} // closed by the first cancel call.
           mu       sync.Mutex
           children map[canceler]struct{} // set to nil by the first cancel call
           err      error                 // set to non-nil by the first cancel call
       }
       
       func (c *cancelCtx) Done() <-chan struct{} {
           return c.done
       }
       
       func (c *cancelCtx) Err() error {
           c.mu.Lock()
           defer c.mu.Unlock()
           return c.err
       }
       
       func (c *cancelCtx) String() string {
           return fmt.Sprintf("%v.WithCancel", c.Context)
       }

Хотя внутренняя реализация конкретной структуры, возвращаемой тремя функциями выше и ниже (все они реализуют интерфейс Context), отличается, она совершенно неизвестна пользователю.

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)    //返回 cancelCtx
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) //返回 timerCtx
    func WithValue(parent Context, key, val interface{}) Context    //返回 valueCtx

providing interception points

Больше нет, нужно добавить

Анализ исходного кода интерфейса

Так много сказал, тогда вы можете вернуться к реализации конкретного исходного кода

базовая структура интерфейса

В зависимости от того, содержит ли интерфейс методы, базовая реализация представлена ​​двумя структурами: iface и eface. eface представляет структуру интерфейса без метода или пустой интерфейс. Для большинства типов данных в Golang структуру _type можно абстрагировать, и для разных типов будет какая-то другая информация.

    type eface struct {
        _type *_type
        data  unsafe.Pointer
    }
    
    type _type struct {
        size       uintptr // type size
        ptrdata    uintptr // size of memory prefix holding all pointers
        hash       uint32  // hash of type; avoids computation in hash tables
        tflag      tflag   // extra type information flags
        align      uint8   // alignment of variable with this type
        fieldalign uint8   // alignment of struct field with this type
        kind       uint8   // enumeration for C
        alg        *typeAlg  // algorithm table
        gcdata    *byte    // garbage collection data
        str       nameOff  // string form
        ptrToThis typeOff  // type for pointer to this type, may be zero
    }
    

iface представляет базовую реализацию непустого интерфейса. По сравнению с пустым интерфейсом, непустой содержит некоторые методы. Конкретная реализация метода хранится в переменной itab.fun.

    type iface struct {
        tab  *itab
        data unsafe.Pointer
    }
    
    // layout of Itab known to compilers
    // allocated in non-garbage-collected memory
    // Needs to be in sync with
    // ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
    type itab struct {
        inter  *interfacetype
        _type  *_type
        link   *itab
        bad    int32
        inhash int32      // has this itab been added to hash?
        fun    [1]uintptr // variable sized
    }

Только представьте, если интерфейс содержит несколько методов, как здесь может быть только одна забавная переменная? На самом деле, декомпилировав сборку, можно увидеть, что компилятор промежуточного процесса преобразует исходный тип данных в соответствии с пустым интерфейсом или непустым интерфейсом нашего целевого типа преобразования (convert to или ). Здесь компилятор определяет, соответствует ли структура требованиям типа интерфейса (то есть реализует ли структура все методы интерфейса).

itab из iface

Наиболее важной из структур iface является структура itab. itab можно понимать как пара . Конечно, itab также содержит некоторую другую информацию, такую ​​как конкретная реализация метода, содержащегося в интерфейсе. Подробности ниже. Структура itab следующая.

    type itab struct {
        inter  *interfacetype
        _type  *_type
        link   *itab
        bad    int32
        inhash int32      // has this itab been added to hash?
        fun    [1]uintptr // variable sized
    }

Тип интерфейса содержит некоторую информацию о самом интерфейсе, такую ​​как путь к пакету и содержащийся в нем метод. Упомянутые выше iface и eface представляют собой структурную структуру объекта после преобразования типа данных (встроенного и определяемого типа) в интерфейс, а тип интерфейса здесь является абстрактным представлением, когда мы определяем интерфейс.

    type interfacetype struct {
        typ     _type
        pkgpath name
        mhdr    []imethod
    }
    
    type imethod struct {   //这里的 method 只是一种函数声明的抽象,比如  func Print() error
        name nameOff
        ityp typeOff
    }
    

_type представляет конкретный тип. Конкретная реализация метода в интерфейсе представлена ​​fun. Например, если тип интерфейса содержит метод A и B, конкретную реализацию этих двух методов можно найти через fun.

расположение памяти интерфейса

Очень важно понимать структуру памяти интерфейса, только поняв это, мы сможем в дальнейшем анализировать эффективность таких ситуаций, как утверждения типов. Давайте посмотрим на пример:

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

Согласно реализации исходного кода вышеприведенного интерфейса можно узнать, что интерфейс фактически состоит из двух элементов в памяти, как показано на рисунке ниже, вкладка указывает на виртуальную таблицу, а данные указывают на фактические данные, на которые ссылаются. Виртуальная таблица отображает фактическую информацию о типе и набор методов, требуемых интерфейсом.

![Выгрузка памяти интерфейса layout_731644.png]

Обратите внимание на структуру itable, сначала некоторые метаданные, описывающие информацию о типе, а затем список указателей на функции, которые удовлетворяют интерфейсу Stringger (обратите внимание, что это не набор указателей на функции фактического типа Binary). Следовательно, если мы вызываем функцию через интерфейс, фактическая операция на самом деле s.tab->fun0. Это похоже на виртуальную таблицу С++? Далее давайте посмотрим на разницу между виртуальной таблицей golang и виртуальной таблицей C++.

Сначала взгляните на C++, он создает набор методов для каждого типа, а его виртуальная таблица на самом деле является самим набором методов или его частью, когда вы сталкиваетесь с множественным наследованием (или при реализации нескольких интерфейсов, это очень распространено), будет быть несколькими указателями виртуальных таблиц в структуре объекта C++, и каждый указатель виртуальной таблицы указывает на другую часть набора методов.Поэтому указатели функций в наборе методов C++ имеют строгий порядок. Многие новички в C++ нервничают, когда сталкиваются с множественным наследованием.Из-за своего метода проектирования, чтобы гарантировать, что его виртуальная таблица может нормально работать, C++ вводит много понятий, что такое виртуальное наследование и проблема интерфейсных функций с тем же именем. , проблема многократного наследования одного и того же интерфейса на разных уровнях и т.д. Даже ветеран может легко написать проблемный код по небрежности.

Давайте посмотрим на реализацию golang.Как и C++, golang также создает набор методов для каждого типа, разница в том, что виртуальная таблица интерфейса специально генерируется во время выполнения. Те, кто может быть внимательным, могут узнать, почему виртуальная таблица генерируется во время выполнения. Поскольку их слишком много, комбинация каждого типа интерфейса и всех типов сущностей, которые удовлетворяют его интерфейсу, является его возможным количеством виртуальных таблиц, на самом деле большинство из них не нужны, поэтому golang выбирает генерировать его во время выполнения, например, когда такое выражение, как s := Stringer(b) встречается в примере впервые, golang сгенерирует виртуальную таблицу, соответствующую бинарному типу интерфейса Stringer, и кэширует ее.

После понимания структуры памяти golang легко проанализировать эффективность таких ситуаций, как утверждение типа.При определении того, удовлетворяет ли тип интерфейсу, golang использует набор методов типа для соответствия набору методов, требуемому интерфейсом. Тип считается удовлетворяющим интерфейсу, если его набор методов полностью содержит набор методов интерфейса. Например, если определенный тип имеет m методов, а интерфейс имеет n методов, легко понять, что временная сложность этого суждения равна O(mXn), но ее можно оптимизировать с помощью предварительной сортировки, а реальную временную сложность равно O(m+n).

интерфейс против нуля

Цитируя темы обсуждения коллег внутри компании, я чувствую, что не понял этого раньше, поэтому я привожу это отдельно, пример является лучшим объяснением, следующим образом

package main

import (
	"fmt"
	"reflect"
)

type State struct{}

func testnil1(a, b interface{}) bool {
	return a == b
}

func testnil2(a *State, b interface{}) bool {
	return a == b
}

func testnil3(a interface{}) bool {
	return a == nil
}

func testnil4(a *State) bool {
	return a == nil
}

func testnil5(a interface{}) bool {
	v := reflect.ValueOf(a)
	return !v.IsValid() || v.IsNil()
}

func main() {
	var a *State
	fmt.Println(testnil1(a, nil))
	fmt.Println(testnil2(a, nil))
	fmt.Println(testnil3(a))
	fmt.Println(testnil4(a))
	fmt.Println(testnil5(a))
}

Результат возврата следующий

false
false
false
true
true

Зачем?

Переменная типа interface{} содержит 2 указателя, один указатель указывает на тип значения, а другой указатель указывает на фактическое значение. Для переменной nil типа interface{} два ее указателя равны 0, но после передачи var a *State указатель указанного типа не равен 0, поскольку существует тип, поэтому сравнение ложно. Сравнение типов интерфейса, если два указателя равны, может быть равным.

["Добро пожаловать, обратите внимание на мою общедоступную учетную запись WeChat: разработка серверной системы Linux, и позже я буду активно отправлять высококачественные статьи через общедоступную учетную запись WeChat"]

我的微信公众号