方法与接口
定义
在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
}
- 相比函数,方法与其的区别在于多了接收者,即
把方法绑定到这个接收者对应的类型上
。- 接收者可以是任何类型,但不能是接口,因为接口是抽象定义,方法确实具体实现。
- 允许出现接收者类型不同,但是方法名相同的方法。
- 接收者类型是指针时使用的是
引用传递
,其他则是值传递
,针对同一个结构体的方法的接收者类型需要统一。- 只有当接收者类型为指针时,可以将
nil作为实参
传入。- 当实参是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) // 将方法表达式作为函数参数传递
}
- 方法变量:接
将方法赋值给一个变量
,不需要指定接收者,只能在方法所属类型的实例上调用,不能在其他类型的实例上调用。- 方法表达式:方法表达式的语法是
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
}
- go中的封装效果等同于Java中的类字段属性设为私有,但是对外提供getter和setter用来修改字段属性。
- 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)
}
1.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()
}
- 接口的动态值:接口的类型信息 & 接口值的具体值或底层对象的副本。
- go的接口是隐式实现,一个类型只需要实现接口所定义的
所有方法
,无需显示声明。- 接口是值类型,可以作为
函数的参数和返回值
,也可以赋值给其他接口变量
。接口类型的零值是nil
。- 接口变量在运行时存放的是
实际的对象和值
,通过类型断言可以将接口变量转换为具体类型,并访问其属性和方法。
接口断言
类型断言是一个作用在接口值上的操作,即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")
}
}