[TOC]
Golang Interface In-depth understanding of the interface
Interface is introduced
If Goroutine and Channel are the two cornerstones of Go concurrency, then interfaces are the key to data types in Go programming. In the actual programming of Go language, almost all data structures are developed around interfaces, which are the core of all data structures in Go language.
Go is not a typical OO language in that it does not support the concepts of classes and inheritance syntactically.
Is it impossible to have polymorphic behavior without inheritance? The answer is no, Go introduced a new type – Interface, which implements the concept of “polymorphism” similar to C++ in effect, although not syntactically equivalent to C++ polymorphism, but at least in the final effect, it has the shadow of polymorphism.
Although the Go language has no concept of classes, the data types it supports can define corresponding methods (s). A method(s) is essentially a function that operates on a data type compared to a normal function, so there is a receiver in the function signature to indicate that the currently defined function operates on that receiver.
Any data type that the Go language supports other than the Interface type can define its method(not just structs), but in real projects, method(s) is defined on structs. From this point of view, we can think of structs in Go as lightweight “classes” that do not support inheritance behavior.
Syntactically, an Interface defines a method(s) or a group of methods (s) that have only a function signature and no implementation code. . Data types that implement functions called “methods” defined in Interface are said to implement an Interface. This is a common OO approach, and here is a simple example
type MyInterface interface{
Print()
}
func TestFunc(x MyInterface) {}
type MyStruct struct {}
func (me MyStruct) Print() {}
func main() {
var me MyStruct
TestFunc(me)
}
Copy the code
Why Interface
Why use interfaces? In Gopher China, He gave the following reasons:
Writing Generic Algorithm
Hiding implementation detail
providing interception points
Here are the three reasons in general
Writing Generic Algorithm
Strictly speaking, generic programming is not supported in Golang. Generic programming has been Golang’s biggest complaint since it is so easy to use in high-level languages like C++. But you can do generic programming with interfaces, as shown in the following reference example
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 heapsortif 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)
}
Copy the code
The Sort function takes an interface and contains three methods: Len(), Less(I,j int), Swap(I,j int). When used regardless of the element type (int, float, string…) As long as we implement these three methods, we can use the Sort function, thus achieving “generic programming”.
I also used this approach in my flash chat project, where I sorted messages.
Here is a concrete example, the code can say everything, at a glance to understand:
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.
typeByAge []Person // define 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"And 31}, {"John", 42},
{"Michael", 17},
{"Jenny", 26},
}
fmt.Println(people)
sort.Sort(ByAge(people))
fmt.Println(people)
}
Copy the code
Hiding implementation detail
Hiding the concrete implementation is easy to understand. For example, if I design a function that returns you an interface, then you can only do something with the methods inside the interface, but the implementation inside is completely unknown.
CancelCtx, timerCtx, valueCtx; cancelCtx cancelCtx, timerCtx, valueCtx;
We just happened to talk about context, so let’s review it
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
Copy the code
CancelCtx struct cancelCtx struct cancelCtx struct cancelCtx struct cancelCtx struct
// 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.
typeCancelCtx struct {Context} cancelCtx struct {Contextdone 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)
}
Copy the code
Although the concrete structs returned by the following three functions (all implementing the Context Interface) are different internally, they are completely imperceptive to the user.
Func WithCancel(parent Context) (CTX Context, cancel CancelFunc) cancelCtx func WithDeadline(parent Context, Deadline time. time) (Context, CancelFunc) // Returns timerCtx func WithValue(parent Context, key, Val interface{}) Context returns valueCtxCopy the code
providing interception points
No more, to be added
Interface source code analysis
Having said so much, then you can look at the implementation of the specific source code
Interface infrastructure
Depending on whether an interface contains a method, the underlying implementation is represented by two structs: iface and eface. Eface represents an interface structure without method, or empty Interface. The _type structure can be abstracted from most of the data types in Golang, and there is some additional information for different types.
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
}
Copy the code
Iface represents the underlying implementation of a non-Empty Interface. In contrast to empty Interface, non-Empty contains some method. The implementation of method is stored in the itab.fun variable.
type iface struct {
tab *itab
data unsafe.Pointer
}
// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be insync 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
}
Copy the code
Imagine if your interface contains multiple methods and you only have one fun variable. In fact, as you can see by discompiling the assembly, the intermediate compiler is going to use the empty interface or non-Empty interface type that we’re converting to, To convert the original data type (to <*_type, unbroadening.Pointer> or <*itab, unbroadening.Pointer>). It is up to the compiler to check whether the struct meets the interface type requirement (i.e., whether the struct implements all methods of the interface).
Iface of itab
The most important iFACE structure is the ITAB structure. Itab = pair<interface type, concrete type> Of course, itAB also contains some other information, such as the implementation of the method contained in the interface. More on that. The structure of itAB is as follows.
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
}
Copy the code
The InterfaceType contains some information about the interface itself, such as the package Path, and the method contained in it. The iface and eface mentioned above are struct structures of entities after the built-in and type-define data types are converted into interfaces. And the interfaceType here is kind of an abstract representation of what we’re defining as an interface.
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
typeStruct {// Method is just an abstraction of a function declaration, such as func Print() error name nameOff ityptypeOff
}
Copy the code
_type stands for concrete type. Fun is the implementation of method in interface. For example, if the interface type contains methods A and B, fun can be used to find the implementation of these two methods.
Interface memory layout
Understanding the memory structure of an interface is essential so that we can further analyze the efficiency of situations such as type assertions. Let’s start with an example:
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())
}
Copy the code
Interface is actually composed of two members, TAB refers to the virtual table, and data refers to the actual referenced data. The virtual table represents the actual type information and the set of methods required by the interface
! [Uploading interface memory layout _731644.png]
Observe the structure of the itable, starting with some metadata describing type information and then a list of function Pointers that satisfy the Stringger interface (note that this is not a set of function Pointers to the actual type Binary). So if we call the function through the interface, the actual operation is actually s.tabb ->fun0. Is it similar to C++ virtual tables? Let’s take a look at how golang’s virtual tables differ from C++ ‘s.
Look at c + +, it created a set of methods for each type, and its virtual table is actually set itself or is it part of the method, when faced with multiple inheritance (or implement multiple interfaces, it is very common), in the structure of c + + object can have multiple virtual table pointer, each different parts of the virtual table pointer to set the method, therefore, Function Pointers in the C++ method set have a strict order. Many C++ novices get nervous when faced with multiple inheritance because of the way it is designed. To ensure that its virtual tables work, C++ introduces concepts such as virtual inheritance, the problem of interface functions having the same name, the problem of the same interface being inherited multiple times at different levels, etc. Even experienced people can easily write bad code by mistake.
Like C++, golang creates a set of methods for each type, except that the interface’s virtual table is generated specifically at run time. Perhaps careful students can see why virtual tables are generated at run time. Because there are so many, the combination of each interface type and all the entity types that satisfy its interface is the number of possible virtual tables, much of which is actually unnecessary, so Golang chooses to generate it at run time. For example, when the example first encountered a statement like S := Stringer(b), Golang generates a Stringer interface virtual table corresponding to the Binary type and caches it.
Understand the golang memory structure, and then to analyze the situation such as types of assertions efficiency problem is easy, when determining a type meets an interface, golang used type of method and interface required by the method sets match, if the type of method completely consists of an interface method sets, can think this type meet the interface. For example, if there are m methods for a type and N methods for an interface, it is easy to know that the time complexity of this decision is O(mXn), but it can be optimized by pre-ordering, and the actual time complexity is O(m+ N).
Interface versus nil
Citing the discussion topics of colleagues in the company, I feel that I did not understand them before, so I listed them separately, and the example is the best explanation, as follows
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() } funcmain() {
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))
}
Copy the code
The result is as follows
false
false
false
true
true
Copy the code
Why?
An interface{} variable contains two Pointers, one to the value type and the other to the actual value. For a nil variable of interface{} type, both Pointers are 0; Var a *State = 0; var a *State = 0; var a *State = 0; If two Pointers are equal, they are equal.
Welcome to follow my wechat official account: Linux server system development, and we will vigorously send quality articles through wechat official account in the future.