> For the complete documentation index, see [llms.txt](https://sunwenfei.gitbook.io/sunwenfei/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sunwenfei.gitbook.io/sunwenfei/golang/golang-ji-chu-jiao-cheng/mian-xiang-dui-xiang-bian-cheng/jie-kou.md).

# 接口

#### 接口是什么？

接口是属于面向对象编程的范畴，**接口描述了对象(类)的行为**。接口仅仅是一个描述，描述对象(或类)应该实现哪些行为。但是具体如何实现接口并不关注，而是由具体对象(或类)决定。

Golang中的接口其实就是一个方法签名的集合。如果某个类型实现了某个接口中的所有方法，那么我们说该类型实现了这个接口。这其实跟面向对象编程中接口的定义与实现非常类似。**接口描述一个类型应该实现的方法集合，但是并不决定具体如何实现，具体的实现还是由具体的类型决定的。**

举个例子，WashingMachine(洗衣机)可以定义为包含`Cleaning()`和`Drying()`方法签名的接口。而所有实现了这两个方法的类型都算是实现了这个接口。

#### 声明一个接口并实现该接口

下面我们声明一个接口，并创建一个类型来实现该接口。

```go
package main

import "fmt"

type VowelsFinder interface {
    FindVowels() []rune
}

type MyString string

func (ms MyString) FindVowels() []rune {
    var vowels []rune
    for _, rune := range ms {
        if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
            vowels = append(vowels, rune)
        }
    }
    return vowels
}

func main() {
    name := MyString("Sam Anderson")
    var v VowelsFinder
    v = name // 由于MyString实现了该VowelsFinder接口，因此可赋值
    fmt.Printf("Vowels are %c", v.FindVowels())
}
```

声明接口的语法如下：

```go
type InterfaceName interface {
    MethodSignature1
    MethodSignature2
    ...
}
```

这里我们声明了一个接口`VowelsFinder`，该接口包含一个方法签名：`FindVowels() []rune`。然后创建了一个类似`MyString`。

**我们给该类型绑定了一个方法`FindVowels() []rune`，这时我们便说`MyString`实现了接口`VowelsFinder`，这跟Java确实不一样，在Java中需要显示的使用关键字`implements`来声明某个类实现了某个接口。Golang中不需要这种显示声明，只要某个类型实现了某个接口中的所有方法，那么该类型就隐式的实现了该接口。**

前面程序中我们将`MyString`类型的变量`name`赋值给了`VowelsFinder`类型的变量`v`。由于`MyString`实现了接口`VowelsFinder`，因此这个赋值是合法的。后面的`v.FindVowels()`调用了`MyString`绑定的方法来打印出了找到的所有元音字母，该程序输出：

```
Vowels are [a e o]
```

这样我们便声明了一个接口，并实现了该接口。

#### 接口的实际应用

前面的例子展示了如何声明一个接口并实现，但是并不实用。我们完全可以调用`name.FindVowels()`，而不是调用`v.FindVowels()`来完成相同的功能，也就是说完全没必要声明一个接口也能实现相同的功能。

现在我们来看一个比较实用的例子，根据公司里每个员工的薪资来计算整个公司的总开销。场景其实很简单，比如公司共有两个员工，那么这两个员工的薪资总和即为公司的总开销，为了简单我们假设薪资的单位统一为USD。

```go
package main

import "fmt"

type SalaryCalculator interface {
    CalculateSalary() int
}

type Permanent struct {
    empId    int
    basicpay int
    pf       int
}

type Contract struct {
    empId    int
    basicpay int
}

func (p Permanent) CalculateSalary() int {
    return p.basicpay + p.pf
}

func (c Contract) CalculateSalary() int {
    return c.basicpay
}

func totalExpense(s []SalaryCalculator) {
    expense := 0
    for _, v := range s {
        expense = expense + v.CalculateSalary()
    }
    fmt.Printf("Total Expense Per Month $%d", expense)
}

func main() {
    pemp1 := Permanent{1, 5000, 20}
    pemp2 := Permanent{2, 6000, 30}
    cemp1 := Contract{3, 3000}
    employees := []SalaryCalculator{pemp1, pemp2, cemp1}
    totalExpense(employees)
}
```

这里我们声明了一个接口`SalaryCalculator`，该接口含一个方法签名`CalculateSalary() int`。接着声明了两种员工类型，`Permanent`和`Contract`，分别表示正式工和合同工。正式工的薪资包含`basicpay`和`pf`两部分，合同工薪资仅包含`basicpay`。`Permanent`和`Contract`均实现了`SalaryCalculator`接口。

函数`totalExpense`接收的入参是`SalaryCalculator`类型的切片，我们可以将实现了该接口的变量存入该切片，比如`Permanent`类型和`Contract`类型。然是在`totalExpense`函数看来，它只关心入参是`SalaryCalculator`类型的切片。由于所有实现了`SalaryCalculator`接口的类型都绑定了`CalculateSalary`方法，因此无论切片中存放的是哪个具体的类型，都可以调用该类型绑定的`CalculateSalary`方法。

这种程序设计的最大好处是可扩展性，假设该公司加入了一种新的员工类型`Freelancer`，即自由职业者，那么只要该类型也实现了接口`SalaryCalculator`即可，这时我们就可以将该类型的变量添加到切片`employees`中，而函数`totalExpense`完全不需要升级。

前面的程序输出结果如下：

```
Total Expense Per Month $14050
```

#### 接口的内部表示

Golang中我们可以把接口看作是由`type`和`value`组成的一个元组`(type, value)`。`type`表示接口内部具体的数据类型，`value`表示接口内部具体的数据值。

这里的**元组**两个字可能不太好理解，这里多说几句，我个人觉得**元组**这个中文翻译比较晦涩难懂，但是也没有更好的词替代...。大家可以这么理解元组：由多个元组成的一个组合。那么这里的元是什么意思呢？其实很简单，它类似于我们初高中学习的**一元二次方程组**中的元，比如`(x, y)`表示一个二维坐标系中的点，这里的`x`和`y`就分别是一元。因此元组这个词一般用来表示类似于`(x, y)`这种多元组合。

从接口的声明语法`type InterfaceName interface {}`也可以看出来，Golang中的接口本质上也是一种数据类型，既然是一种数据类型，我们就可以声明该类型的变量，比如`var v VowelsFinder`，然后我们会给该变量赋值，比如前面的：

```go
name := MyString("Sam Anderson")
var v VowelsFinder
v = name
```

因此接口类型变量`v`内部有一个具体数据类型`MyString`和具体值`"Sam Anderson"`。

我们实现一个例子来理解下：

```go
package main

import "fmt"

type Tester interface {
    Test()
}

type MyFloat float64

func (m MyFloat) Test() {
    fmt.Println(m)
}

func describe(t Tester) {
    fmt.Printf("Interface type %T value %v\n", t, t)
}

func main() {
    var t Tester
    f := MyFloat(89.7)
    t = f
    describe(t)
    t.Test()
}
```

这里的接口`Tester`含有一个方法签名`Test()`。我们声明了一个该接口类型的变量`var t Tester`，由于`MyFloat`类型实现了该接口，因此我们将`MyFloat`类型的变量`f`赋值给变量`t`，这时`t`内部的具体类型即为`MyFloat`，具体值即为`89.7`。`describe`函数打印出了具体类型和具体值。该程序输出结果如下：

```
Interface type main.MyFloat value 89.7
89.7
```

#### 空接口

如果一个接口没有一个方法签名，那么我们称这个接口为空接口。空接口可以简短的表示为`interface {}`。由于空接口不含方法签名，因此相当于所有类型都实现了空接口。

```go
package main

import "fmt"

func describe(i interface{}) {
    fmt.Printf("Type = %T, value = %v\n", i, i)
}

func main() {
    s := "Hello World"
    describe(s)
    i := 55
    describe(i)
    strt := struct {
        name string
    }{
        name: "Naveen R",
    }
    describe(strt)
}
```

这里函数`describe(i interface{})`接收的入参类型是一个空接口，因此我们可以传入任何类型的参数。 我们分别传入了`string`、`int`以及`struct`，该程序执行结果如下：

```
Type = string, value = Hello World
Type = int, value = 55
Type = struct { name string }, value = {Naveen R}
```

#### 类型断言

类型断言指的是断言某个接口类型变量底层具体的数据类型。语法如下：

```go
i.(T)
```

其中`T`为断言(猜测)的底层具体数据类型，`i.(T)`的返回值是底层`T`类型的具体数据值。我们通过一个例子来理解下：

```go
package main

import "fmt"

func assert(i interface{}) {
    v := i.(int)
    fmt.Println(v)
}

func main() {
    var s interface{} = 56
    assert(s)
}
```

这里接口变量`s`的具体类型为`int`。我们通过`i.(int)`来获取接口变量`i`内部的具体值。该程序输出为：`56`。

前面断言成功是因为`s`确实是`int`类型，但是假设`s`不是`int`类型会怎么样呢？我们尝试如下：

```go
package main

import "fmt"

func assert(i interface{}) {
    s := i.(int)
    fmt.Println(s)
}

func main() {
    var s interface{} = "Steven Paul"
    assert(s)
}
```

这里接口变量`s`内部的具体数据类型是`string`，我们尝试断言为`int`类型，或尝试获取其内部值，执行会有运行时错误：

```
panic: interface conversion: interface {} is string, not int
```

可以改用如下方式解决：

```go
v, ok := i.(T)
```

如果接口变量`i`的内部具体数据类型确实是`T`，则`v`即为内部的具体值，`ok`为`true`；反之，则`v` 被赋值为`T`类型的零值，`ok`为`false`。并且程序不会报错。

```go
package main

import "fmt"

func assert(i interface{}) {
    v, ok := i.(int)
    fmt.Println(v, ok)
}
func main() {
    var s interface{} = 56
    assert(s)
    var i interface{} = "Steven Paul"
    assert(i)
}
```

该程序执行结果如下：

```
56 true
0 false
```

#### 类型 switch

`类型switch`用来判断某个接口内部具体的数据类型是否是多个数据类型中的某一个，并执行匹配上的语句。语法类似于普通的`switch`语句，但是普通switch语句比较的是一个值是否匹配，而`类型switch`语句比较的是一个类型是否匹配。 `类型匹配`的具体语法我们通过一个具体例子来看下：

```go
package main

import "fmt"

func findType(i interface{}) {
    switch i.(type) {
    case string:
        fmt.Printf("I am a string and my value is %s\n", i.(string))
    case int:
        fmt.Printf("I am a int and my value is %d\n", i.(int))
    default:
        fmt.Printf("Unknown type\n")
    }
}

func main() {
    findType("Naveen")
    findType(77)
    findType(89.98)
}
```

这里`switch i.(type)`即为一个`类型switch`语句，该语句内每一个`case`语句用来判断`i`的具体类型是否是某个类型。该程序执行结果如下：

```
I am a string and my value is Naveen
I am a int and my value is 77
Unknown type
```

前面程序中的`89.98`具体类型为`float64`，因此未匹配到前两种情况，最后的默认`case`得以执行。

**我们不仅能判断某个接口变量是否匹配某个具体类型，比如前面的`int`、`string`，还能判断某个接口变量是否匹配某个接口类型。** 举例如下：

```go
package main

import "fmt"

type Describer interface {
    Describe()
}
type Person struct {
    name string
    age  int
}

func (p Person) Describe() {
    fmt.Printf("%s is %d years old", p.name, p.age)
}

func findType(i interface{}) {
    switch v := i.(type) {
    case Describer:
        v.Describe()
    default:
        fmt.Printf("unknown type\n")
    }
}

func main() {
    findType("Naveen")
    p := Person{
        name: "Naveen R",
        age:  25,
    }
    findType(p)
}
```

这里`string`类型并未实现接口`Describer`，因此执行默认`case`。而结构体类型`Person`实现了接口`Describer`，因此`case Describer`得以匹配成功，接着执行`v.Describe()`。该程序执行结果如下：

```
unknown type
Naveen R is 25 years old
```

#### 实现接口：指针接收器 vs 值接收器

对于接口中声明的某个方法签名，我们既可以基于`值接收器`又可基于`指针接收器`实现。但是具体用哪种来实现，还是得分场景，这点在前面**方法**一节已经提到过：

> 那么何时使用值接收器，何时又使用指针接收器呢？其实很简单，这本质上还是指针传递和值传递的问题。如果你不想通过指针来改变方法外部定义的变量，那么就使用值传递，否则就使用指针传递。但是用值传递会将变量整体拷贝一份，对于比较大的结构体变量开销比较大，这种情况下考虑到性能可以使用指针接收器。

下面我们举个例子：

```go
package main

import "fmt"

type User interface {
    SetIndex(int)
}

type UserMutable struct {
    Index int
}

// UserMutable DOES NOT implement User, *UserMutable implements User
func (u *UserMutable) SetIndex(index int) {
    u.Index = index
}

type UserImmutable struct {
    Index int
}

// UserImmutable implements User
func (u UserImmutable) SetIndex(index int) {
    u.Index = index
}

func main() {
    //userMutable is type *UserMutable
    userMutable := &UserMutable{1}
    // but not userMutable := UserMutable{1}
    var user User
    user = userMutable // 这时user内部具体类型是指针类型
    user.SetIndex(9)

    //userImmutable is type UserImmutable
    userImmutable := UserImmutable{1}
    // or userImmutable := &UserImmutable{1}
    var user2 User
    user2 = userImmutable
    user2.SetIndex(9)

    fmt.Println("userMutable", userMutable.Index)
    // "userMutable 9"
    fmt.Println("userImmutable", userImmutable.Index)
    // "userImmutable 1"
}
```

这里我们声明了两个类型`UserMutable`和`UserImmutable`。从名字也能看出来，`UserMutable`表示一种可变类型，我们可通过指针来改变该结构体的属性。

而`UserImmutable`表示不可变类型，在其上调用绑定的方法时，由于是值接收器，因此相当于将值拷贝到了一个新的变量上，在函数内部的任何操作都是对这个新拷贝变量的操作，并不会影响原结构体变量。

**前面的章节我们已经了解到，Golang提供了非常方便的语法糖来调用结构体绑定的方法，可以通过变量本身或者变量的指针来调用，Golang根据方法接收器具体类型是指针类型还是值类型来帮我们把代码转换一下。但是这点在接口这里不是完全适用。对于指针接收器定义的方法，我们仅能通过指针来访问，无法通过变量来访问。**

举例来说就是前面程序中的：

```go
userMutable := &UserMutable{1}
// but not userMutable := UserMutable{1}
var user User
user = userMutable
```

由于Golang中的接口无法寻址内部具体的变量，因此我们不能寄托于Golang帮我们自动解析出接口内部具体变量地址。具体为什么接口无法寻址内部具体的变量，可以参考[这里](https://github.com/golang/go/wiki/MethodSets#interfaces)和这个[问答](https://stackoverflow.com/questions/48790663/why-value-stored-in-an-interface-is-not-addressable-in-golang)。

前面的程序输出如下：

```go
userMutable 9
userImmutable 1
```

需要说明一下，这里将原文中的两节接口相关内容合成了一节。刚刚使用的例子来自[这里](https://paradite.com/2015/10/14/golang-interface-method-pointer-receiver-or-value-receiver/)，跟原文中的例子并非一个，我个人觉得更加合适就直接替换了。

#### 实现多个接口

Golang中一个类型可以实现多个接口，我们举个例子：

```go
package main

import "fmt"

type SalaryCalculator interface {
    DisplaySalary()
}

type LeaveCalculator interface {
    CalculateLeavesLeft() int
}

type Employee struct {
    firstName   string
    lastName    string
    basicPay    int
    pf          int
    totalLeaves int
    leavesTaken int
}

func (e Employee) DisplaySalary() {
    fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}

func (e Employee) CalculateLeavesLeft() int {
    return e.totalLeaves - e.leavesTaken
}

func main() {
    e := Employee{
        firstName:   "Naveen",
        lastName:    "Ramanathan",
        basicPay:    5000,
        pf:          200,
        totalLeaves: 30,
        leavesTaken: 5,
    }
    var s SalaryCalculator = e
    s.DisplaySalary()
    var l LeaveCalculator = e
    fmt.Println("\nLeaves left =", l.CalculateLeavesLeft())
}
```

该程序声明了两个接口类型，`SalaryCalculator`和`LeaveCalculator`。由于`Employee`结构体类型同时实现了这两个接口中所有方法，因此我们说`Employee`同时实现了这两个接口。因此该结构体类型的变量`e`可以分别赋值给这两种接口类型的变量，两次赋值都是合法的。我们可以这样理解，将结构体变量`e`赋值给不同的接口变量相当于将该结构体变量分别看作不同的接口类型变量。这就好比我们既可以将一个`智能手机`看作是`通讯工具`(因为它能用来`打电话`)，也可以看作是`mp4`(因为它能用来`播放视频`)，还可以看作是`游戏机`(因为它能用来`玩游戏`)，这时我们说`智能手机`这个类型同时实现了`通讯工具`、`mp4`以及`游戏机`的接口。

#### 接口嵌套

Golang不支持继承，但是支持接口嵌套，即将多个接口嵌套在另外一个接口内，或者说将多个接口组合在一块生成一个新接口。我们通过一个例子来理解下。

```go
package main

import "fmt"

type SalaryCalculator interface {
    DisplaySalary()
}

type LeaveCalculator interface {
    CalculateLeavesLeft() int
}

type EmployeeOperations interface {
    SalaryCalculator
    LeaveCalculator
}

type Employee struct {
    firstName   string
    lastName    string
    basicPay    int
    pf          int
    totalLeaves int
    leavesTaken int
}

func (e Employee) DisplaySalary() {
    fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}

func (e Employee) CalculateLeavesLeft() int {
    return e.totalLeaves - e.leavesTaken
}

func main() {
    e := Employee{
        firstName:   "Naveen",
        lastName:    "Ramanathan",
        basicPay:    5000,
        pf:          200,
        totalLeaves: 30,
        leavesTaken: 5,
    }
    var empOp EmployeeOperations = e
    empOp.DisplaySalary()
    fmt.Println("\nLeaves left =", empOp.CalculateLeavesLeft())
}
```

这里接口`EmployeeOperations`是由两个子接口`SalaryCalculator`和`LeaveCalculator`内嵌组合生成的。那么如果一个类型同时实现了`SalaryCalculator`和`LeaveCalculator`接口，我们就说它实现了`EmployeeOperations`接口，建议暂停几秒钟，好好理解下这句话。该程序中`Employee`同时实现了`SalaryCalculator`和`LeaveCalculator`接口，因此也就是实现了`EmployeeOperations`接口。因此我们可以将`Employee`类型的变量`e`赋值给声明好的`EmployeeOperations`接口类型变量`empOp`。该程序输出如下：

```
Naveen Ramanathan has salary $5200
Leaves left = 25
```

#### 接口类型变量的零值

Golang中所有数据类型的变量都有一个默认的零值，即如果近声明了某个类型的变量，而不进行初始化，那么Golang自动帮我们初始化为对应类型的零值，接口类型也不例外。 接口变量的零值为`nil`，其内部具体的类型和值也是`nil`。

```go
package main

import "fmt"

type Describer interface {
    Describe()
}

func main() {
    var d1 Describer
    if d1 == nil {
        fmt.Printf("d1 is nil and has type %T value %v\n", d1, d1)
    }
}
```

这里的`d1`仅仅进行了声明，并没有手动初始化，因此被自动初始化为零值`nil`，该程序输出如下：

```
d1 is nil and has type <nil> value <nil>
```

如果我们尝试在调用`nil`接口变量的某个方法，那么程序会报运行时错误，因为`nil`的内部具体类型和变量也是`nil`。

```go
package main

type Describer interface {
    Describe()
}

func main() {
    var d1 Describer
    d1.Describe()
}
```

该程序执行报如下运行时错误：

```
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x104e15f]
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://sunwenfei.gitbook.io/sunwenfei/golang/golang-ji-chu-jiao-cheng/mian-xiang-dui-xiang-bian-cheng/jie-kou.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
