Skip to content

方法与接口

方法定义

在 Go 中没有类的概念,一般用 struct 来代替类的操作,但 struct 中只包含字段属性,所以 Go 提供了一种特殊的函数,其通过 作用在某个接收者上面来实现与其关联

go
// func (接收者 接收者类型) 方法名 (参数列表) 返回值类型 {}

func main() {
    p := Person{"jack", 18}
    fmt.Printf("person: %p, person name: %s\n", &p, p.Name)
    p.printPerson()      // 基于值复制的传参
    (&p)._printPerson()  // 基于指针的传参
    p._printPerson()     // 与 (&p) 效果相同,因为编译器会做隐式的转换

    var p1 *Person
    p1._printPerson()  // 当指针类型作为接收者时,类型可以为 nil,值类型不可以为 nil
}

func (p Person) printPerson() {  // person 作为接收者类型
    fmt.Printf("person: %p, person name: %s\n", &p, p.Name)
}

// 允许 nil 作为接收者
func (p *Person) _printPerson() {  // 指针作为接收者类型
    if p == nil {
        fmt.Printf("person: %p", p)
        return
    }
    (*p).Name = "lucy"
    fmt.Printf("person: %p, person name: %v\n", p, (*p).Name)
}

type Person struct {
    Name string
    Age  int
}
  1. 相比函数,方法与它的区别在于多了接收者,即 把方法绑定到这个接收者对应的类型上
  2. 接收者可以是任何类型,但不能是接口,因为接口是抽象定义,方法是具体实现
  3. 允许出现接收者类型不同,但是方法名相同的方法
  4. 接收者类型是指针时使用的是 引用传递,其他则是 值传递,针对同一个结构体的方法的接收者类型需要统一
  5. 只有当接收者类型为指针时,可以将 nil 作为实参 传入
  6. 当实参是 T 类型的变量而形参的接收者是 *T 类型时,编译器会隐式的获取变量的地址

结构体内嵌的类型

go
type Inner struct{ A, B int }

func (i Inner) inner() {
    fmt.Printf("Inner: A: %d, B: %d\n", i.A, i.B)
}

func (o Outer) outer() {
    fmt.Printf("Outer: C: %s\n", o.C)
}

type Outer struct {
    Inner
    C string
}

func test3() {
    o := &Outer{Inner{1, 2}, "3"}
    o.outer()
    o.inner()  // 作用等同于 o.Inner.inner()
}

通过结构体的嵌套,可以无感的调用内嵌的结构体的方法。

方法变量和方法表达式

go
type Person struct {
    Name string
    Age  int
}

func (p *Person) modifyPerson() {  // 指针作为实参的接收者类型
    fmt.Printf("person: %#v\n", p)  // 打印接收者参数
}

func test5(f func(*Person)) {  // 方法表达式作为形参需要指定类型
    p := &Person{"lucky", 18}
    f(p)
}

func main() {
    p := &Person{"jack", 20}
    person := p.modifyPerson  // 获取方法变量,只能在方法所属的实例 p 上调用
    person()                  // 调用方法 效果等同于 p.modifyPerson() 不需要指定接收者

    person2 := (*Person).modifyPerson  // 获取方法表达式
    person2(p)                         // 调用方法表达式时需要传入接收者实参

    test5(person2)  // 将方法表达式作为函数参数传递
}
  1. 方法变量:将方法赋值给一个变量,不需要指定接收者,只能在方法所属类型的实例上调用,不能在其他类型的实例上调用
  2. 方法表达式:方法表达式的语法是 T.methodName,需要 显式指定接收者类型 作为第一个参数。可以作为普通函数一样传递

封装

如果变量或者方法不是通过对象访问到的,这称作 封装的变量或者方法(又称数据隐藏)

go
// 在另一个包中调用 Logger 的方法
func test6() {
    l := &Logger{}
    l.SetFlags(1)
    fmt.Printf("flags: %d", l.Flags())
}

// Logger 定义实体,但字段是当前包私有,无法导出的
type Logger struct {
    flags  int
    prefix string
}

// Flags 等同于 Java 中字段的 getFlags 方法,Go 默认去掉 get,且可导出,所以方法名大写
func (logger *Logger) Flags() int {
    return logger.flags
}

func (logger *Logger) SetFlags(flag int) {
    logger.flags = flag
}
  1. Go 中的封装效果等同于 Java 中的类字段属性设为私有,但是对外提供 getter 和 setter 用来修改字段属性
  2. Go 中的 getter 方法是默认去掉 get 的,比如 getAge 在 Go 中就是 Age,注意需要大写(因为需要对其他包导出)

接口

接口类型是对其他类型行为的概括和抽象。相比 Java 的显示声明,Go 的独特之处在于它是 隐式实现,对于一个具体的类型,无须声明它实现了哪些接口,只需要提供接口所必须的方法即可。

go
// interface{} 效果等同于 Java 中的 Object
func test1(args ...interface{}) {
    fmt.Println(args)
}

// any 等同于 interface{}
func test2(args ...any) {
    fmt.Println(args)
}

Go 中提供了一种接口类型,即 interface{},等价于 Java 的 Object。1.18 版本后可以使用 any 替换,效果是等价的。

go
type Object interface {
    equals(source any, target any) bool  // 此处不需要定义接收者类型,与给 struct 定义方法不同
    Say                                   // 接口嵌套接口
}

type Say interface {
    doSay(something string) (word string)
}

type Phone interface {
    Call()
}

type Iphone struct{}
type Android struct{}

func (i Iphone) Call() {  // 方法名、参数与返回值都必须相同
    fmt.Println("i'm iphone!")
}

func (a Android) Call() {
    fmt.Println("i'm android!")
}

func test3() {
    var phone Phone  // 声明接口类型的变量
    phone = new(Iphone)
    phone.Call()

    phone = new(Android)
    phone.Call()
}
  1. 接口的动态值:接口的类型信息 & 接口值的具体值或底层对象的副本
  2. Go 的接口是隐式实现,一个类型只需要实现接口所定义的 所有方法,无需显示声明
  3. 接口是值类型,可以作为 函数的参数和返回值,也可以 赋值给其他接口变量。接口类型的零值是 nil
  4. 接口变量在运行时存放的是 实际的对象和值,通过类型断言可以将接口变量转换为具体类型,并访问其属性和方法

接口断言

类型断言是一个作用在接口值上的操作,即 x.(T),其中 x 是一个接口类型的表达式,T 是一个类型(称为断言类型)。即 x 的动态类型是否是 T

go
func test5() {
    var p Phone
    p = Iphone{}
    i := p.(Iphone)  // 接口断言:如果 p 的动态类型是 Iphone,那么会返回 p 的动态值
    i.getIphone()    // 调用 Iphone 类型独有的方法(非实现的接口方法)

    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("s0 print error: %s\n", r)
        }
    }()
    s0 := p.(Say)  // 会产生 panic
    s0.doSay("hello")

    if s, ok := p.(Say); ok {  // ok 返回 bool 值,表示 p 的类型是否是 Say
        fmt.Printf("%#v\n", s)
    } else {
        fmt.Println("panic: interface conversion")
    }
}