likes
comments
collection
share

go 普通方法与接口方法

作者站长头像
站长
· 阅读数 37

个人主页:二郎腿 (erlangtui.top)

一,基本介绍

  • 方法能给用户自定义的类型添加新的行为,它和函数的区别在于方法有一个接收者,可以理解为方法是被指定了一个固定参数的函数
  • 这个参数通常叫做接收者,接收者的类型可以是值类型,也可以是指针类型,就像是函数的参数类型一样;
  • 自定义类型大部分都为结构体类型,后续全部以结构体类型举例;

二、接收者的类型

  • 方法的接收者可以是值类型接收者,也可以是指针类型的接收者,无论接收者是什么类型的方法,调用方法的时候,都是通过值拷贝的方式将该类型的变量值传递给方法的接收者,与函数的传参是一样的原理,接收者都是原变量的拷贝;
  • 接收者是指针类型的方法时,在方法内改变接收者的元素可以影响到原变量,因为接收者和原变量都是指针,指向相同的地址;
  • 接收者是值类型的方法时,在方法内修改接收者的元素不会影响原变量,因为修改的是原变量的副本;
  • 值类型的变量既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型的变量既可以调用指针接收者的方法,也可以调用值接收者的方法;
  • 在调用方法的时候,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型

三、普通方法

1,值类型接收者

  • 接收者是值类型时,无论调用方是值类型还是指针类型,在方法内对元素的更新都无法影响到调用方,示例如下;
  • 当是值类型的调用方时,实际传递的是调用方自己,形参是调用方的副本;当是指针类型的调用方时,实际会对指针解引用,获取其指向的值,然后传递该值;
package main

import "fmt"

type person struct {
	age int
}
// 接收者是值类型的方法
func (p person) getAge() int {
	return p.age
}
// 接收者是值类型的方法
func (p person) setAge(a int) {
	p.age = a
}

func main() {
	p1 := person{age: 18}

	// 调用方是值类型
	fmt.Println(p1.getAge()) // 18

	// 调用方是值类型
	p1.setAge(19)
	fmt.Println(p1.getAge()) // 18

	// ----------------------

	p2 := &person{age: 100}

	// 调用方是指针类型
	fmt.Println(p2.getAge()) // 100

	// 调用方是指针类型
	p2.setAge(99)
	fmt.Println(p2.getAge()) // 100
}
  • 以上示例的方法等同于下面示例的函数:
package main

import "fmt"

type person struct {
	age int
}

// 形参是值类型的函数
func getAge(p person) int {
	return p.age
}

// 形参是值类型的函数
func setAge(p person, a int) {
	p.age = a
}

func main() {
	p1 := person{age: 18}

	// 原变量是值类型,传递其自己
	fmt.Println(getAge(p1)) // 18

	// 原变量是值类型,传递其自己
	setAge(p1, 19)
	fmt.Println(getAge(p1)) // 18

	// ----------------------

	p2 := &person{age: 100}

	// 原变量是指针类型,传递其指向的变量
	fmt.Println(getAge(*p2)) // 100

	// 原变量是指针类型,传递其指向的变量
	setAge(*p2, 99)
	fmt.Println(getAge(*p2)) // 100
}

2,指针类型接收者

  • 接收者是指针类型时,无论调用方是值类型还是指针类型,在方法内对接收者元素的更新都将影响到调用方,示例如下;
  • 当是值类型的调用方时,实际传递的是调用方的引用;当是指针类型的调用方时,实际传递的是调用方自己,形参还是一个指针;
package main

import "fmt"

type person struct {
	age int
}

// 接收者是指针类型的方法
func (p *person) getAge() int {
	return p.age
}

// 接收者是指针类型的方法
func (p *person) setAge(a int) {
	p.age = a
}

func main() {
	p1 := person{age: 18}

	// 调用方是值类型
	fmt.Println(p1.getAge()) // 18

	// 调用方是值类型
	p1.setAge(19)
	fmt.Println(p1.getAge()) // 19

	// ----------------------

	p2 := &person{age: 100}

	// 调用方是指针类型
	fmt.Println(p2.getAge()) // 100

	// 调用方是指针类型
	p2.setAge(99)
	fmt.Println(p2.getAge()) // 99
}
  • 以上示例的方法等同于下面示例的函数:
package main

import "fmt"

type person struct {
	age int
}

// 形参是指针类型的函数
func getAge(p *person) int {
	return p.age
}

// 形参是指针类型的函数
func setAge(p *person, a int) {
	p.age = a
}

func main() {
	p1 := person{age: 18}

	// 原变量是值类型,传递其引用
	fmt.Println(getAge(&p1)) // 18

	// 原变量是值类型,传递其引用
	setAge(&p1, 19)
	fmt.Println(getAge(&p1)) // 19

	// ----------------------

	p2 := &person{age: 100}

	// 原变量是指针类型,传递其自己
	fmt.Println(getAge(p2)) // 100

	// 原变量是指针类型,传递其自己
	setAge(p2, 99)
	fmt.Println(getAge(p2)) // 99
}

四、实现接口的方法

  • 当实现了一个接收者是值类型的方法,可以自动生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者;
  • 指针变量副本与原来的指针指向一个相同并且唯一的结构体,所以编译器可以隐式的对变量解引用获取指针指向的结构体;
  • 以下代码能够正常编译和运行:
package main

type user interface {
	get() string
	set(s string)
}

type gopher struct {
	name string
}

func (g gopher) get() string {
	return g.name
}

func (g gopher) set(s string) {
	g.name = s
}

func f1() {
	var c user = &gopher{"go1"}
	c.get()
	c.set("f1")
}

func f2() {
	var c user = gopher{"go2"}
	c.get()
	c.set("f2")
}

func main() {
	f1()
	f2()
}
  • 当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的更新(通过指针实现)将无法实现,因为值类型会产生一个拷贝,不会真正影响调用者
  • 如果实现了接收者是指针类型的方法,当传入一个结构体值时,这个结构体是原始结构体的副本即使能够生成一个结构体指针,也是指向副本的指针,无法对原始结构体产生改变;
  • 以下代码就编译出错:
package main

import "fmt"

type user interface {
	get() string
	set(n string)
}

type gopher struct {
	name string
}

func (g *gopher) get() string {
	return g.name
}

func (g *gopher) set(n string) {
	g.name = n
}

func f1() {
	var c user = &gopher{"go1"}
	fmt.Println(c.get())
	c.set("f1")
	fmt.Println(c.get())
}

func f2() {
	var c user = gopher{"go2"} // 此处编译出错
	fmt.Println(c.get())
	c.set("f2")
	fmt.Println(c.get())
}

func main() {
	f1()
	f2()
}

command-line-arguments

./main.go:30:6: cannot use gopher{...} (type gopher) as type user in assignment:

gopher does not implement user (get method has pointer receiver)

  • 实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;
  • 实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法;

五、两者的区别

  • 普通方法:
    • 普通方法是针对特定类型定义的方法,可以在类型实例上直接调用这些方法;
    • 接收者是指针类型的方法,也能接收调用者是值类型的调用,使用了调用者值的引用来调用方法;
  • 接口方法:
    • 接口方法是一种协议,在接口中定义了方法签名,但没有具体的实现,具体实现由类型自身完成;
    • 对于接口方法,需要先将实例赋值给接口类型的变量,然后通过接口变量来调用方法
    • 如果一个类型实现了接口中定义的所有方法,那么该类型就实现了这个接口,实现接口的类型可以被赋值给接口类型的变量,从而实现了多态性
    • 接收者是指针类型的接口方法,无法接收调用者是值类型的调用,默认没有实现值类型的方法,因为值类型的方法修改的是调用者的副本,无法对原始调用者进行修改;
  • 当接收者是 map、chan 类型时,需要通过 make 来创建,并经过类型转换为定义的类型才可以调用其方法,否则会 panic;

六、使用场景

1,接收者必须是指针

  • 方法需要修改接收者本身;
  • 方法接收者包含了一个不能被复制到成员,如 sync 包的内容;

2,接收者应该是指针

  • 当接收者是一个很大的对象时,使用指针能够避免过大复制开销,可以通过基准测试判断;
  • 当接收者的可变部分不是接收者的直接成员,而是成员的子成员,

3,接收者必须是值

  • 强制方法的接收者不能被方法更改;

4,接收者应该是值

  • 接收者不需要被方法更新时;
  • 接收者是有基础类型构成的小数组或者结构体,并且不饱和可变的成员时;
  • 接收者是想 int、float64、string 这样的基础类型时;

七、检测接口的实现

  • 使用结构体时并不关心它实现了哪些接口,Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查;
  • 编译器能够检测接口的方法是否已经实现,赋值语句会发生隐式地类型转换,在转换的过程中,编译器会检测等号右边的类型是否实现了等号左边接口所规定的函数;
package main

import "io"

type myWriter struct {

}
// func (w *myWriter) Write(p []byte) (n int, err error) {
// 	return
// }

func main() {
    // 检查 *myWriter 类型是否实现了 io.Writer 接口
    var _ io.Writer = (*myWriter)(nil)

    // 检查 myWriter 类型是否实现了 io.Writer 接口
    var _ io.Writer = myWriter{}
}
转载自:https://juejin.cn/post/7376186315643846665
评论
请登录