Go编程基础-7. 接口

56

接口

接口类型是对其他类型的抽象和概括,允许编写灵活通用的函数,不依赖特定类型实现。Go语言的接口通过隐式实现,即类型不需要显式声明实现哪些接口,只要实现了接口所需方法即视为实现该接口。这种设计使得不修改已有类型内部实现的前提下,可以为它们添加新接口,尤其适用于无法修改的包中的类型。

接口是一种抽象类型,它不公开所包含数据的布局或内部结构,也不提供这些数据的基本操作。接口仅仅定义了一组方法。如果你得到一个接口类型的值,你无法知道它的具体类型,只能知道它所能执行的方法。换句话说,你只能知道它具备哪些功能,而不是它的内部实现。

1. 接口即约定

// 以Fprintf为例, 其参数为io.Writer接口
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)

// io.Writer接口要求实现符合要求的write函数
type Writer interface {
    Write(p []byte) (n int, err error)
}

因此,Fprintf在实际调用时,其第一个参数可以是任意实现了Write函数的类型,例如:os.File, bytes.Buffer等等。

io.Writer 接口定义了 Fprintf 与调用者之间的约定。一方面,该约定要求调用者提供的具体类型(例如 os.File 或 bytes.Buffer)必须包含一个在签名和行为上与 Write 方法一致的方法。另一方面,这个契约保证了 Fprintf 能够使用任何实现了 io.Writer 接口的参数。Fprintf 只需要能够调用参数的 Write 方法,而不需要知道其写入的目标是文件还是内存。

2. 接口类型

一个接口类型定义了一套方法,如果一个具体类型要实现该接口,则必须实现接口定义中的所有方法。在go语言中,可以在实现了接口的类型变量 赋值 给接口变量:

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

即使是类型的nil指针,也实现了对应接口,因此:

// 类型转换:
var _ io.Writer = (*bytes.Buffer)(nil)

也是合法的语句。

2.1 空接口类型

对于接口类型interface{}, 它不包含任何方法,也无法从其中获得任何信息。但也正因如此,我们可以把任何值赋给空接口类型。靠它才可以让fmt.Println、errorf这类的函数能够接受任意类型的参数。

当然,对于传入的空接口类型,我们无法直接从中获取任何信息,需要通过类型断言的方式,还原出实际的类型和对象再执行操作。

2.2 接口属性

需要注意的是,go语言的接口只有方法没有属性。我们需要通过getter/setter方法的形式,来规定实现类型需要具备的一些基本属性。

// go语言的Getter方法一般省略前缀的Get,直接以属性名的方式获取
type Text interface {
    Pages() int
    Words() int
    PageSize() int
}

3. 使用flag.Value来解析参数

在go语言中,我们可以使用flag.Value来定义命令行标志,来解析和读取程序执行时的命令行参数。flag.Value接口定义如下:

package flag

type Value interface {
    String() string
    Set(string) error
}

其中,String方法用于格式化输出标志对应的值,可以用于输出命令行帮助信息。Set方法解析了传入的字符串参数并更新标志值。(即Getter&Setter)

3.1 案例

在下述案例中,我们来实现flag.Value接口,来读取命令行中输入的参数:

package __interface

import (
	"flag"
	"fmt"
)

// 1. 定义一个flag, 实现flag.Value接口
type celsiusFlag struct {
	Celsius float64
}

func (f *celsiusFlag) Set(celsius string) error {
	n, err := fmt.Sscanf(celsius, "%fC", &f.Celsius)
	if n != 1 || err != nil {
		return fmt.Errorf("can't parse celsius %q", celsius)
	}
	return err
}

func (f *celsiusFlag) String() string {
	return fmt.Sprintf("%.1fC", f.Celsius)
}

// 2. 设计一个函数,对外暴露该类型的初始化方法
func CelsiusFlag(name string, value float64, usage string) *float64 {
	// 2.1 初始化一个默认值
	f := celsiusFlag{value}
	// 2.2 将自定义Flag注册到命令行接口上,进行参数的匹配和解析
	flag.CommandLine.Var(&f, name, usage)
	// 2.3 将结果返回
	return &f.Celsius
}

测试代码:

import (
	"flag"
	"fmt"
	"testing"
)

var temp = CelsiusFlag("temp", 20.0, "the celsius temp")

func TestCelsiusFlag(t *testing.T) {
	// 1. 进行参数的解析
	flag.Parse()
	// 2. 获取并输出结果
	fmt.Println(*temp)
}

命令行参数的解析流程如下:

  1. 初始化全局变量:var temp = CelsiusFlag(“temp”, 20.0, “the celsius temp”) 被执行,temp 被初始化并注册。
  2. 解析参数:flag.Parse() 被调用,flag 包开始解析命令行参数。
  3. 调用Set方法:flag.Parse() 识别到 -temp=“25.0C” 参数,并调用 celsiusFlag 的 Set 方法,解析并赋值给 celsiusFlag.Celsius 字段。
  4. 打印结果:测试函数继续执行,输出解析后的温度值。

4. 接口值

从概念上讲,一个接口类型的值(简称接口值)实际上包含两个部分:一个具体类型和该类型的一个值。这两个部分被称为接口的动态类型和动态值

在像 Go 这样静态类型的语言中,类型仅仅是一个编译时的概念,因此类型本身并不是一个值。在我们的概念模型中,我们使用类型描述符来提供每个类型的具体信息,例如它的名字和方法。对于一个接口值,其类型部分就是通过相应的类型描述符来表示的。

案例:

var w io.Writer
w = os.Stdout

alt text

在Go语言中,变量总是初始化为一个特定的值,接口也不例外。接口的零值是将其动态类型和值都设置为 nil。第二个语句将一个 *os.File 类型的值赋给了 w,这次赋值将一个具体类型隐式转换为一个接口类型,它等价于显式转换 io.Writer(os.Stdout)。无论这种类型转换是隐式的还是显式的,它都可以转换操作数的类型和值。接口值的动态类型会设置为指针类型 *os.File 的类型描述符,其动态值会设置为 os.Stdout 的副本,即一个指向代表进程标准输出的 os.File 类型的指针。调用该接口值的 Write 方法,会实际调用 (*os.File).Write 方法。

一般来说,在编译时我们无法知道一个接口值的动态类型会是什么,所以通过接口来进行调用必然需要使用动态分发。编译器必须生成一段代码来从类型描述符中获取名为 Write 的方法地址,再间接调用该方法地址。调用的接收者就是接口值的动态值,即 os.Stdout,所以实际效果与直接调用 os.Stdout.Write([]byte("hello")) 等价。

4.1 接口值的比较

接口值可以用 ==!= 操作符进行比较。如果两个接口值都是 nil 或者它们的动态类型完全一致且动态值相等(使用动态类型的 == 操作符进行比较),那么这两个接口值相等。由于接口值可以比较,因此它们可以作为 map 的键,也可以作为 switch 语句的操作数。

需要注意的是,在比较两个接口值时,如果两个接口值的动态类型一致,但对应的动态值是不可比较的(例如 slice),那么这种比较会导致崩溃。例如:

var x interface{} = []int{2, 3}
fmt.Println(x == x) // 宕机:试图比较不可比较的类型 []int

从这点来看,接口类型是非平凡的。其他类型要么是可以安全比较的(例如基础类型和指针),要么是完全不可比较的(例如 slice、map 和函数)。但是,当比较接口值或者包含接口值的聚合类型时,我们必须小心崩溃的可能性。当把接口作为 map 的键或者 switch 语句的操作数时,也存在类似的风险。仅在确认接口值包含的动态值可以比较时,才比较接口值。

当处理错误或者调试时,获取接口值的动态类型是很有帮助的。可以使用 fmt 包来实现这个需求:

var w io.Writer
fmt.Printf("%T\n", w) // "<nil>"
w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"
w = new(bytes.Buffer)
fmt.Printf("%T\n", w) // "*bytes.Buffer"

在内部实现中,fmt 包使用反射来获取接口动态类型的名字。

PS: 空接口 != 含有空指针的非空接口, 前者动态类型为nil, 后者有具体的动态类型。

5. 案例

5.1 使用sort.Interface来排序

与字符串格式化类似,排序也是许多程序中广泛使用的操作。尽管一个最简化的快速排序(Quicksort)实现可能仅需15行代码,但一个健壮的实现往往要复杂得多。因此,每次需要排序时重新编写或复制排序代码显然是不切实际的。

幸运的是,sort包提供了针对任意序列根据任意排序函数进行原地排序的功能。这种设计其实并不常见。在许多编程语言中,排序算法通常与序列数据类型绑定,而排序方式则与序列元素类型绑定。相比之下,Go语言的 sort.Sort 函数对序列及其元素的布局没有任何要求,它通过 sort.Interface 接口来指定通用排序算法和具体序列类型之间的契约。

这个接口的实现确定了序列的具体布局(通常是一个切片),以及元素的期望排序方式。一个原地排序算法需要知道三点信息:序列的长度、如何比较两个元素,以及如何交换两个元素的位置。因此,sort.Interface 接口包含以下三个方法:

package sort

type Interface interface {
    Len() int
    Less(i, j int) bool // i, j 是序列元素的下标
    Swap(i, j int)
}

要对序列进行排序,需要先定义一个实现了上述三个方法的类型,然后将 sort.Sort 函数应用到该类型的实例上。

播放列表定义:

// Track 代表音乐曲目
type Track struct {
	Title  string
	Artist string
	Album  string
	Year   int
	Length time.Duration
}

func length(s string) time.Duration {
	d, err := time.ParseDuration(s)
	if err != nil {
		panic(s)
	}
	return d
}

func printTracks(tracks []*Track) {
	const format = "%v\t%v\t%v\t%v\t%v\t\n"
	tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0)
	fmt.Fprintf(tw, format, "Title", "Artist", "Album", "Year", "Length")
	fmt.Fprintf(tw, format, "-----", "------", "-----", "----", "------")
	for _, t := range tracks {
		fmt.Fprintf(tw, format, t.Title, t.Artist, t.Album, t.Year, t.Length)
	}
	tw.Flush() // 计算各列宽度并输出表格
}

排序方式定义(一种排序方式对应着一种sort.Interface的实现):

// 定义一种tracks的排序方式
type byArtist []*Track

func (x byArtist) Len() int {
	return len(x)
}

func (x byArtist) Less(i, j int) bool {
	return x[i].Artist < x[j].Artist
}

func (x byArtist) Swap(i, j int) {
	x[i], x[j] = x[j], x[i]
}

测试代码:

func TestPrintTracks(t *testing.T) {
	tracks := []*Track{
		{"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")},
		{"Go", "Moby", "Moby", 1992, length("3m37s")},
		{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
		{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
	}
	printTracks(tracks)
}

func TestSort(t *testing.T) {
	tracks := []*Track{
		{"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")},
		{"Go", "Moby", "Moby", 1992, length("3m37s")},
		{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
		{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
	}
	// 正向
	sort.Sort(byArtist(tracks))
	printTracks(tracks)

	// 反向
	reverse := sort.Reverse(byArtist(tracks))
	sort.Sort(reverse)
	printTracks(tracks)
}

5.2 http.Handler接口

例如:net/http包实现了简单的web服务器,其通过Handler接口处理请求

package http
type Handler interface {
    ServeHTTP(w Responsewriter, r *Request)
}
func ListenAndServe(address string, h Handler) error

理论上一个服务器只能绑定一个handler, 但是我们可以通过多工转发器ServeMux的形式,根据请求URI将不同的请求转发到不同的处理函数上,来实现不同的行为:

func main() {
 mux := http.NewServeMux()
 mux.Handle("/list", http.HandlerFunc(list))
 mux.Handle("/price", http.HandlerFunc(price))
 log.Fatal(http.ListenAndServe("localhost:8000", mux))
}

一个ServeMux (实现了Handler接口,内部进行请求的分发) 把多个http.Handler 组合成单个http.Handler。在这里,我们再次看到满足同一个接口的多个类型是可以互相替代的,Web服务器可以把请求分发到任意一个http.Handler的实现, 而不用管后面具体的类型是什么。

对于一个更复杂的应用,多个ServeMux会组合起来,用来处理更复杂的分发需求。

5.3 error接口

实际上,go语言中的error只是一个接口类型,包含一个返回错误消息的方法:

type error interface {
    Error() string
}

构造错误最简单的方法是调用 errors.New,它会返回一个包含指定错误消息的新 error 实例。完整的 errors 包只有如下4行代码:

package errors

func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    text string
}

func (e *errorString) Error() string {
    return e.text
}

底层的 errorString 类型是一个结构体,而不是直接使用字符串,这主要是为了避免将来无意间(或者有预谋的)进行布局变更。满足 error 接口的是 *errorString 指针,而不是原始的 errorString,这是为了确保每次调用 New 分配的 error 实例都是独立的。

直接调用 errors.New 比较少见,因为有一个更易用的封装函数 fmt.Errorf,它还额外提供了字符串格式化功能:

package fmt

import "errors"

func Errorf(format string, args ...interface{}) error {
    return errors.New(Sprintf(format, args...))
}

尽管 *errorString 可能是最简单的 error 类型,但类似的简单 error 类型远不止一个。例如,syscall 包提供了 Go 语言的底层系统调用 API。在很多平台上,它还定义了一个满足 error 接口的数字类型 Errno。在 UNIX 平台上,ErrnoError 方法会从一个字符串表中查询错误码对应的错误消息。

package syscall

type Errno uintptr // 操作系统错误码

var errors = [...]string{
    1: "operation not permitted", // EPERM
    2: "no such file or directory", // ENOENT
    3: "no such process", // ESRCH
    // ...
}

func (e Errno) Error() string {
    if 0 <= int(e) && int(e) < len(errors) {
        return errors[e]
    }
    return fmt.Sprintf("errno %d", e)
}

这样,即使 *errorString 是最简单的错误类型,Go 语言中仍存在其他多种实现错误接口的方式。

6. 类型断言

类型断言是一个作用在接口值上的操作,形式类似于 x.(T),其中 x 是一个接口类型的表达式,而 T 是一个类型(称为断言类型)。类型断言会检查操作数的动态类型是否满足指定的断言类型。

有两种可能的情况:

  1. 具体类型断言

    • 判断对象x是否是具体类型T, 例如对于一个空接口对象 interface{}, 判断是否是int。如果是,则返回一个具体类型T的对象。如果检查失败,程序会崩溃(单返回值情况)。例如:
    var w io.Writer
    w = os.Stdout
    f := w.(*os.File) // 成功:f == os.Stdout
    c := w.(*bytes.Buffer) // 崩溃:接口持有的是 *os.File, 不是 *bytes.Buffer
    
  2. 接口类型断言

    • 判断对象x是否是接口类型T, 例如对于一个空接口对象 interface{}, 判断是否是Stringer。如果是,则返回一个接口类型T的对象,该对象的动态类型+动态值不变。例如:
    var w io.Writer
    w = os.Stdout
    rw := w.(io.ReadWriter) // 成功:*os.File 有 Read 和 Write 方法
    w = new(ByteCounter)
    rw = w.(io.ReadWriter) // 崩溃:*ByteCounter 没有 Read 方法
    

无论断言类型是什么,如果操作数是一个空接口值,类型断言都会失败。很少需要从一个接口类型向一个方法更少的、更宽松的类型做类型断言,因为这些方法是其子集。

除了单返回值的类型断言,还有双返回值的情况,其中第一个返回值为转换后的类型对象,第二个返回值为类型断言的接口(是否符合该类型),这种情况下如果类型不符不会崩溃。例如:

var w io.Writer = os.Stdout
f, ok := w.(*os.File) // 成功:ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // 失败:!ok, b == nil

按照惯例,第二个返回值通常赋给一个名为 ok 的变量。如果操作失败,okfalse,第一个返回值为断言类型的零值,在这个例子中就是 *bytes.Buffer 的空指针。ok 返回值通常马上用来决定下一步的操作。下面的 if 表达式可以让代码更紧凑:

if f, ok := w.(*os.File); ok {
  // ...使用 f...
}

当类型断言的操作数是一个变量时,你可能会看到返回值的名字与操作数变量名一致,原有的值会被新的返回值掩盖。例如:

if w, ok := w.(*os.File); ok {
  // ...使用 w...
}

6.1 使用类型断言进行错误判断

一个更可靠的错误定义和处理方法是使用专门的类型来表示结构化的错误值。os 包定义了一个 PathError 类型,用于表示在与文件路径相关的操作(如 OpenDelete)上发生的错误,类似地,LinkError 用于表示在与两个文件路径相关的操作(如 SymlinkRename)上发生的错误。

下面是 os.PathError 的定义:

package os

// PathError 记录了错误以及错误相关的操作和文件路径
type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

许多客户端忽略了 PathError 的结构信息,而是采用统一的方法来处理所有错误,即调用 Error 方法。PathErrorError 方法只是简单地拼接了所有字段,而 PathError 的结构则保留了所有底层信息。对于需要区分错误的客户端,可以使用类型断言来检查错误的特定类型,因为这些类型包含的细节远远超过简单的字符串。例如:

err := os.Open("/no/such/file")
fmt.Println(err) // "open /no/such/file: No such file or directory"
fmt.Printf("%#v\n", err)
// 输出:&os.PathError{Op:"open", Path:"/no/such/file", Err:0x2}

下面的 IsNotExist 函数判断错误是否是一个 *PathError,并且底层错误是下述两者之一: syscall.ENOENTos.ErrNotExist

import (
    "errors"
    "syscall"
)

var ErrNotExist = errors.New("file does not exist")

// IsNotExist 返回一个布尔值,表明错误是否表示文件或目录不存在
func IsNotExist(err error) bool {
    if pe, ok := err.(*PathError); ok {
        err = pe.Err
    }
    return err == syscall.ENOENT || err == ErrNotExist
}

实际使用情况如下:

err := os.Open("/no/such/file")
fmt.Println(os.IsNotExist(err)) // "true"

当然,如果错误消息已被 fmt.Errorf 这类方法合并到一个大字符串中,那么 PathError 的结构信息就丢失了。因此,错误识别通常必须在失败操作发生时立即处理,而不是等到错误消息返回给调用者之后。

7. 类型分支

接口有两种不同的风格。

第一种风格的典型例子包括 io.Readerio.Writerfmt.Stringersort.Interfacehttp.Handlererror。在这种风格下,接口上的各种方法突出了满足这个接口的具体类型之间的相似性,但隐藏了各个具体类型的布局和各自特有的功能。这种风格强调的是方法,而不是具体类型。

第二种风格则充分利用了接口值能够容纳各种具体类型的能力,它将接口作为这些类型的联合(union)来使用。在这种风格中,类型断言用于在运行时区分这些类型(类型分支)并分别处理。这里强调的是满足这个接口的具体类型,而不是接口的方法(有时接口甚至没有方法),也不注重信息隐藏。这种风格的接口使用方式被称为可识别联合(discriminated union)。

示例:可识别联合

假设我们有一个接口 Shape,并且我们希望处理不同的几何形状,例如圆形和矩形。我们可以定义如下接口和具体类型:

package main

import (
    "fmt"
    "math"
)

// 定义接口
type Shape interface{}

// 定义具体类型
type Circle struct {
    Radius float64
}

type Rectangle struct {
    Width, Height float64
}

// 计算面积函数
func Area(s Shape) float64 {
    switch v := s.(type) {
    case Circle:
        return math.Pi * v.Radius * v.Radius
    case Rectangle:
        return v.Width * v.Height
    default:
        return 0
    }
}

func main() {
    shapes := []Shape{
        Circle{Radius: 5},
        Rectangle{Width: 4, Height: 6},
    }

    for _, shape := range shapes {
        fmt.Printf("Area: %f\n", Area(shape))
    }
}

在这个例子中,Shape 接口没有任何方法。我们通过类型断言+类型分支在运行时区分具体类型 CircleRectangle,并分别计算它们的面积。这种使用方式正是可识别联合的典型例子。

类型分支

类型分支的最简单形式与普通的分支语句类似,它们之间的主要区别在于类型分支的操作数形式为 x.(type)(注意:这里直接使用关键词 type,而不是一个具体的类型)。每个分支对应一个或多个类型。类型分支的判定基于接口值的动态类型,其中 nil 分支需要满足 x == nil,而 default 分支则在其他分支都不满足时执行。示例如下:

switch x.(type) {
case nil:
    // ...
case int, uint:
    // ...
case bool:
    // ...
case string:
    // ...
default:
    // ...
}

与普通的 switch 语句类似,类型分支也是按顺序判定的。当一个分支条件满足时,对应的代码会执行。分支的顺序在一个或多个分支为接口类型时尤为重要,因为可能多个分支都能满足条件。default 分支的位置是无关紧要的。此外,类型分支不允许使用 fallthrough(在 Go 中,每个分支执行完默认会 break,如果需要当前分支执行完继续执行下一个分支,需要在当前分支末尾加上 fallthrough 关键词)。

需要注意的是,在某些代码中,分支逻辑需要访问由类型断言提取出来的原始类型值以进行处理操作。这种需求比较常见,因此类型分支语句也有一种扩展形式,用来将每个分支中提取出来的原始值绑定到一个新的变量:

switch x := x.(type) {
    // ...
}

这里将新的变量也命名为 x,与类型断言类似,重用变量名是常见做法。与 switch 语句类似,类型分支也隐式创建了一个词法块,因此声明一个新变量 x 并不会与外部块中的变量 x 冲突。每个分支也会隐式创建各自的词法块。例如:

func sqlQuote(x interface{}) string {
    switch x := x.(type) {
    case nil:
        return "NULL"
    case int, uint:
        return fmt.Sprintf("%d", x) // 这里 x 类型为 interface{}
    case bool:
        if x {
            return "TRUE"
        }
        return "FALSE"
    case string:
        return sqlQuoteString(x) // (未显示具体代码)
    default:
        panic(fmt.Sprintf("unexpected type %T: %v", x, x))
    }
}

在这个例子中,每个 switch 分支都能够访问由类型断言提取出来的原始类型值,并根据具体的类型进行相应的处理。

8. 接口使用建议

当设计一个新包时,很多人往往会先创建一系列接口,然后再定义满足这些接口的具体类型。这种方式会产生很多接口,但这些接口通常只有一个实现。不要这样做。这种接口是不必要的抽象,并且会增加运行时的开销。可以使用导出机制(参考第 6.6 节)来限制一个类型的方法或结构体的字段对包外的可见性。仅在有两个或更多具体类型需要按统一方式处理时才需要接口。

当然,这条规则也有例外。如果出于依赖的原因,接口和类型实现不能放在同一个包里,那么一个接口只有一个具体类型实现也是可以接受的。在这种情况下,接口是一种解耦两个包的好方式。

因为接口仅在有两个或多个类型满足的情况下存在,所以它必然会抽象掉那些特有的实现细节。这种设计的结果是具有更简单和更少方法的接口,例如 io.Writerfmt.Stringer,它们都只有一个方法。设计新类型时,越小的接口越容易满足。一个不错的接口设计经验是仅要求你需要的。

本章关于方法和接口的讲解就到此结束。Go 语言能很好地支持面向对象编程风格,但这并不意味着你只能使用这种风格。不是所有东西都必须是一个对象,全局函数(例如:fmt.Printf)和不完全封装的数据类型也应有它们的位置。

8. 补充

8.1 值接收者 和 类型接收者

在 Go 中,接口实现的一个关键点是接收者类型(receiver type)。接口定义本身并不会限制接收者到底是指针或者类型。 当方法实现为指针接收者(pointer receiver)时,只有这个类型的指针实现了该接口,该类型本身并不算实现该接口(无法作为接口变量进行赋值和传递)。而当方法实现为值接收者(value receiver)时,类型的值和指针都算作实现该接口。

类型和类型指针在接口实现上的区别:

  1. 值接收者:
type MyInterface interface {
    MyMethod()
}

type MyStruct struct {}

// 值接收者方法
func (m MyStruct) MyMethod() {
    // 实现
}

var v MyStruct
var p *MyStruct

// 都可以调用 MyMethod
v.MyMethod()
p.MyMethod()

// 都可以赋值给接口
var i MyInterface = v
var j MyInterface = p
  1. 指针接收者:
type MyInterface interface {
    MyMethod()
}

type MyStruct struct {}

// 指针接收者方法
func (m *MyStruct) MyMethod() {
    // 实现
}

var v MyStruct
var p *MyStruct = &v

// 只能通过指针调用 MyMethod
p.MyMethod()

// 只能通过指针赋值给接口
var i MyInterface = p

// 不能通过值调用 MyMethod
// v.MyMethod() // 编译错误
// var j MyInterface = v // 编译错误