banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

【Reprint】Basics of Interfaces in Go Language

title: 【Reprint】Basics of Go Language Interfaces
date: 2021-08-09 16:41:33
comment: false
toc: true
category:

  • Golang
    tags:
  • Reprint
  • Go
  • Basics
  • Interfaces

This article is reprinted from: Basics of Go Language Interfaces | Li Wenzhou's Blog


An interface defines the behavior specification of an object, only defining the specification without implementing it, with specific objects implementing the details of the specification.

Interface#

Interface Type#

In Go language, an interface is a type, an abstract type.

interface is a collection of methods, embodying duck-type programming. What an interface does is like defining a protocol (rules); as long as a machine has the functions of washing and spinning, I call it a washing machine. It does not care about attributes (data), only about behaviors (methods).

To protect your Go language career, please remember that an interface is a type.

Why Use Interfaces#

type Cat struct{}  

func (c Cat) Say() string { return "Meow Meow Meow" }  

type Dog struct{}  

func (d Dog) Say() string { return "Woof Woof Woof" }  

func main() {  
	c := Cat{}  
	fmt.Println("Cat:", c.Say())  
	d := Dog{}  
	fmt.Println("Dog:", d.Say())  
}  

In the above code, cats and dogs are defined, and they both can make sounds. You will notice that there is clearly repeated code in the main function. If we later add pigs, frogs, and other animals, our code will continue to repeat. Can we treat them as "animals that can make sounds"?

Similar examples are often encountered in our programming process:

For example, an online mall may use Alipay, WeChat, UnionPay, and other methods for online payment. Can we treat them as "payment methods"?

For example, triangles, quadrilaterals, and circles can all calculate perimeter and area. Can we treat them as "shapes"?

For example, sales, administration, and programmers can all calculate monthly salaries. Can we treat them as "employees"?

To solve similar problems as above, the Go language has designed the concept of interfaces. Interfaces differ from all the specific types we have seen before; an interface is an abstract type. When you see a value of an interface type, you do not know what it is; the only thing you know is what can be done through its methods.

Definition of Interface#

Go language advocates programming to interfaces.

Each interface consists of several methods, and the definition format of an interface is as follows:

type InterfaceTypeName interface {  
    MethodName1( ParameterList1 ) ReturnValueList1  
    MethodName2( ParameterList2 ) ReturnValueList2  

}  

Where:

  • Interface Name: Use type to define the interface as a custom type name. In Go language, when naming interfaces, it is common to add er to the end of the word, such as an interface for writing operations called Writer, and an interface with string functionality called Stringer, etc. The interface name should ideally highlight the type meaning of the interface.
  • Method Name: When the first letter of the method name is uppercase and the first letter of this interface type name is also uppercase, this method can be accessed by code outside the package where the interface is located.
  • Parameter List, Return Value List: The parameter variable names in the parameter list and return value list can be omitted.

For example:

type writer interface {  
    Write([]byte) error  
}  

When you see this interface type value, you do not know what it is; the only thing you know is that you can do something through its Write method.

Conditions for Implementing an Interface#

An object implements an interface as long as it implements all the methods in the interface. In other words, an interface is a list of methods that need to be implemented.

Let's define a Sayer interface:

// Sayer interface  
type Sayer interface {  
    say()  
}  

Define two structures, dog and cat:

type dog struct {}  

type cat struct {}  

Since the Sayer interface only has one say method, we only need to implement the say method for dog and cat to implement the Sayer interface.

// dog implements the Sayer interface  
func (d dog) say() {  
    fmt.Println("Woof Woof Woof")  
}  

// cat implements the Sayer interface  
func (c cat) say() {  
    fmt.Println("Meow Meow Meow")  
}  

Implementing an interface is that simple; as long as all methods in the interface are implemented, the interface is implemented.

Interface Type Variables#

So what is the use of implementing an interface?

Interface type variables can store all instances that implement that interface. For example, in the above example, a variable of type Sayer can store variables of type dog and cat.

func main() {  
    var x Sayer // Declare a variable x of type Sayer  
    a := cat{}  // Instantiate a cat  
    b := dog{}  // Instantiate a dog  
    x = a       // Can directly assign the cat instance to x  
    x.say()     // Meow Meow Meow  
    x = b       // Can directly assign the dog instance to x  
    x.say()     // Woof Woof Woof  
}  

Tips: Observe the following code and appreciate the clever use of _

// Excerpt from gin framework routergroup.go  
type IRouter interface{ ... }  

type RouterGroup struct { ... }  

var _ IRouter = &RouterGroup{}  // Ensure RouterGroup implements the IRouter interface  

Difference Between Value Receiver and Pointer Receiver in Implementing Interfaces#

What is the difference between using a value receiver to implement an interface and using a pointer receiver? Let's look at an example to see the difference.

We have a Mover interface and a dog structure.

type Mover interface {  
    move()  
}  

type dog struct {}  

Value Receiver Implementing Interface#

func (d dog) move() {  
    fmt.Println("The dog can move")  
}  

At this point, the implementing interface is of type dog:

func main() {  
    var x Mover  
    var wangcai = dog{} // Wangcai is of type dog  
    x = wangcai         // x can accept dog type  
    var fugui = &dog{}  // Fugui is of type *dog  
    x = fugui           // x can accept *dog type  
    x.move()  
}  

From the above code, we can see that after implementing the interface using a value receiver, both the dog structure and the pointer to the structure *dog type variable can be assigned to the interface variable. This is because Go language has syntax sugar for dereferencing pointer type variables; the dog pointer fugui will automatically dereference to *fugui.

Pointer Receiver Implementing Interface#

Let's test the same code again to see what the difference is when using a pointer receiver:

func (d *dog) move() {  
    fmt.Println("The dog can move")  
}  
func main() {  
    var x Mover  
    var wangcai = dog{} // Wangcai is of type dog  
    x = wangcai         // x cannot accept dog type  
    var fugui = &dog{}  // Fugui is of type *dog  
    x = fugui           // x can accept *dog type  
}  

At this point, the Mover interface is implemented by *dog type, so x cannot accept the dog type wangcai; x can only store values of type *dog.

Interview Question#

Note: This is a question where you need to answer "can" or "cannot"!

First, please observe the following piece of code, and then answer whether this code can compile:

type People interface {  
    Speak(string) string  
}  

type Student struct{}  

func (stu *Student) Speak(think string) (talk string) {  
    if think == "sb" {  
        talk = "You are a big handsome guy"  
    } else {  
        talk = "Hello"  
    }  
    return  
}  

func main() {  
    var peo People = Student{}  
    think := "bitch"  
    fmt.Println(peo.Speak(think))  
}  

Relationship Between Types and Interfaces#

A Type Implements Multiple Interfaces#

A type can implement multiple interfaces simultaneously, and the interfaces are independent of each other, unaware of each other's implementations. For example, a dog can bark and can move. We define the Sayer interface and the Mover interface as follows:

// Sayer interface  
type Sayer interface {  
    say()  
}  

// Mover interface  
type Mover interface {  
    move()  
}  

A dog can implement both the Sayer interface and the Mover interface.

type dog struct {  
    name string  
}  

// Implement Sayer interface  
func (d dog) say() {  
    fmt.Printf("%s can bark Woof Woof Woof\n", d.name)  
}  

// Implement Mover interface  
func (d dog) move() {  
    fmt.Printf("%s can move\n", d.name)  
}  

func main() {  
    var x Sayer  
    var y Mover  

    var a = dog{name: "Wangcai"}  
    x = a  
    y = a  
    x.say()  
    y.move()  
}  

Multiple Types Implementing the Same Interface#

In Go language, different types can also implement the same interface. First, we define a Mover interface that requires a move method.

// Mover interface  
type Mover interface {  
    move()  
}  

For example, a dog can move, and a car can also move, which can be implemented with the following code:

type dog struct {  
    name string  
}  

type car struct {  
    brand string  
}  

// dog type implements Mover interface  
func (d dog) move() {  
    fmt.Printf("%s can run\n", d.name)  
}  

// car type implements Mover interface  
func (c car) move() {  
    fmt.Printf("%s moves at 70 mph\n", c.brand)  
}  

At this point, we can treat both dogs and cars as moving objects in the code, without needing to care about what they specifically are; we just need to call their move methods.

func main() {  
    var x Mover  
    var a = dog{name: "Wangcai"}  
    var b = car{brand: "Porsche"}  
    x = a  
    x.move()  
    x = b  
    x.move()  
}  

The execution result of the above code is as follows:

Wangcai can run  
Porsche moves at 70 mph  

Moreover, a method of an interface does not necessarily need to be fully implemented by one type; the methods of an interface can be implemented by embedding other types or structures in the type.

// WashingMachine  
type WashingMachine interface {  
    wash()  
    dry()  
}  

// Dryer  
type dryer struct {}  

// Implement the dry() method of the WashingMachine interface  
func (d dryer) dry() {  
    fmt.Println("Spin it")  
}  

// Haier Washing Machine  
type haier struct {  
    dryer // Embed dryer  
}  

// Implement the wash() method of the WashingMachine interface  
func (h haier) wash() {  
    fmt.Println("Washing")  
}  

Interface Nesting#

Interfaces can create new interfaces through nesting.

// Sayer interface  
type Sayer interface {  
    say()  
}  

// Mover interface  
type Mover interface {  
    move()  
}  

// Interface nesting  
type animal interface {  
    Sayer  
    Mover  
}  

The usage of the nested interface is the same as that of a normal interface. Here we let cat implement the animal interface:

type cat struct {  
    name string  
}  

func (c cat) say() {  
    fmt.Println("Meow Meow Meow")  
}  

func (c cat) move() {  
    fmt.Println("The cat can move")  
}  

func main() {  
    var x animal  
    x = cat{name: "HuaHua"}  
    x.move()  
    x.say()  
}  

Empty Interface#

Definition of Empty Interface#

An empty interface is an interface that does not define any methods. Therefore, any type implements the empty interface.

Variables of empty interface types can store variables of any type.

func main() {  
    // Define an empty interface x  
    var x interface{}  
    s := "Hello Shahe"  
    x = s  
    fmt.Printf("type:%T value:%v\n", x, x)  
    i := 100  
    x = i  
    fmt.Printf("type:%T value:%v\n", x, x)  
    b := true  
    x = b  
    fmt.Printf("type:%T value:%v\n", x, x)  
}  

Application of Empty Interface#

Empty Interface as Function Parameter#

Using an empty interface allows a function to accept parameters of any type.

// Empty interface as function parameter  
func show(a interface{}) {  
    fmt.Printf("type:%T value:%v\n", a, a)  
}  

Empty Interface as Map Value#

Using an empty interface allows a dictionary to store any value.

// Empty interface as map value  
var studentInfo = make(map[string]interface{})  
studentInfo["name"] = "Shahe Nazha"  
studentInfo["age"] = 18  
studentInfo["married"] = false  
fmt.Println(studentInfo)  

Type Assertion#

An empty interface can store values of any type, so how do we retrieve the specific data it stores?

Interface Value#

An interface value (short for interface value) consists of a concrete type and the value of the concrete type. These two parts are called the interface's dynamic type and dynamic value, respectively.

Let's look at a specific example:

var w io.Writer  
w = os.Stdout  
w = new(bytes.Buffer)  
w = nil  

To check the value in the empty interface, we can use type assertion, with the syntax format:

x.(T)  

Where:

  • x: Represents a variable of type interface{}
  • T: Represents the type that x may be asserted to.

This syntax returns two parameters; the first parameter is the variable after x is converted to type T, and the second value is a boolean value. If it is true, it indicates that the assertion was successful; if false, it indicates that the assertion failed.

For example:

func main() {  
    var x interface{}  
    x = "Hello Shahe"  
    v, ok := x.(string)  
    if ok {  
        fmt.Println(v)  
    } else {  
        fmt.Println("Type assertion failed")  
    }  
}  

In the above example, if multiple assertions are needed, multiple if checks must be written. At this point, we can use the switch statement to implement:

func justifyType(x interface{}) {  
    switch v := x.(type) {  
    case string:  
        fmt.Printf("x is a string, value is %v\n", v)  
    case int:  
        fmt.Printf("x is an int, value is %v\n", v)  
    case bool:  
        fmt.Printf("x is a bool, value is %v\n", v)  
    default:  
        fmt.Println("Unsupported type!")  
    }  
}  

Because of the characteristic that an empty interface can store values of any type, the use of empty interfaces in Go language is very widespread.

It is important to note that interfaces should only be defined when there are two or more concrete types that must be processed in the same way. Do not write interfaces just for the sake of interfaces, as this will only increase unnecessary abstraction and lead to unnecessary runtime overhead.

Exercise#

Use interfaces to implement a simple logging library that can write logs to both the terminal and a file.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.