3

Go 学习笔记6-Go方法

 1 year ago
source link: https://codeshellme.github.io/2022/06/go6/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go 语言中的方法和面向对象中的方法并不是一样的。Go 引入方法这一元素,并不是要支持面向对象编程范式,而是 Go 践行组合设计哲学的一种实现层面的需要。

Go 语言中的方法的本质就是,一个以方法的 receiver 参数作为第一个参数的普通函数

1,Go 方法的定义

Go 方法的形式,比函数多了一个 receiver

在这里插入图片描述

receiver 参数也是方法与类型之间的纽带,也是方法与函数的最大不同。

Go 中的方法必须是归属于一个类型的,而 receiver 参数的类型就是这个方法归属的类型。

注意 ListenAndServeTLS 是 *Server 类型的方法,而不是 Server 类型的方法。

方法的一般声明形式:

func (t *T或T) MethodName(参数列表) (返回值列表) {
// 方法体
  • 无论 receiver 参数的类型为 *T 还是 T,我们都将 T 叫做 t 的基类型
  • 如果 t 的类型为 T,那么说这个方法是类型 T 的一个方法
  • 如果 t 的类型为 *T,那么就说这个方法是类型 *T 的一个方法
  • receiver 参数的基类型本身不能为指针类型或接口类型
  • 每个方法只能有一个 receiver 参数
    • Go 不支持在方法的 receiver 部分放置包含多个 receiver 参数的参数列表,或者变长 receiver 参数
  • 方法声明要与 receiver 参数的基类型声明放在同一个包内
    • 我们无法为原生类型(诸如 int、float64、map 等)添加方法
    • 不能跨越 Go 包为其他包的类型声明新方法

Go 方法的调用形式:

type T struct{}
func (t T) M(n int) {
func main() {
var t T
t.M(1) // 通过类型T的变量实例调用方法M
p := &T{}
p.M(2) // 通过类型*T的变量实例调用方法M

我们可以将方法作为右值,赋值给一个函数类型的变量,比如下:

type T struct {
func (t T) Get() int {
return t.a
func (t *T) Set(a int) int {
t.a = a
return t.a
func main() {
var t T
f1 := (*T).Set // f1的类型,也是*T类型Set方法的类型:func (t *T, int)int
f2 := T.Get // f2的类型,也是T类型Get方法的类型:func(t T)int
fmt.Printf("the type of f1 is %T\n", f1) // the type of f1 is func(*main.T, int) int
fmt.Printf("the type of f2 is %T\n", f2) // the type of f2 is func(main.T) int
f1(&t, 3)
fmt.Println(f2(t)) // 3

2,receiver 参数的类型问题

来看下面例子中的两个 Go 方法,以及它们等价转换后的函数:

func (t T) M1() <=> F1(t T)
func (t *T) M2() <=> F2(t *T)
  • M1 方法是 receiver 参数类型为 T 的一类方法的代表
    • T 类型实例的 receiver 参数以值传递方式传递到 M1 方法体中
    • 实际上是 T 类型实例的副本,M1 方法体中对副本的任何修改操作,都不会影响到原 T 类型实例
  • M2 方法是 receiver 参数类型为 *T 的另一类
    • *T 类型实例的 receiver 参数以值传递方式传递到 M2 方法体中
    • 实际上是 T 类型实例的地址,M2 方法体通过该地址可以对原 T 类型实例进行任何修改操作

一个示例:

package main
type T struct {
func (t T) M1() {
t.a = 10
func (t *T) M2() {
t.a = 11
func main() {
var t T
println(t.a) // 0
t.M1()
println(t.a) // 0
p := &t
p.M2()
println(t.a) // 11

选择 receiver 参数类型的原则:

  1. 如果 Go 方法要把对 receiver 参数代表的类型实例的修改,反映到原类型实例上,那么我们应该选择 *T 作为 receiver 参数的类型。
  2. 如果我们不需要在方法中对类型实例进行修改呢?这个时候我们是为 receiver 参数选择 T 类型还是 *T 类型呢?
    1. 一般情况下,我们会为 receiver 参数选择 T 类型,这样可以减少外部修改类型实例内部状态的“渠道”
    2. 如果 receiver 参数类型的 size 较大,以值拷贝形式传入就会导致较大的性能开销,这时我们选择 *T 作为 receiver 类型可能更好些
  3. T 类型是否要实现某一接口
    1. 如果 T 类型需要实现某一接口的全部方法,那么我们就需要使用 T 作为 receiver 参数的类型来满足接口类型方法集合中的所有方法。
    2. 如果 T 类型不需要实现某一接口,那么我们就可以参考原则一和原则二来为 receiver 参数选择类型了。

无论是 T 类型实例,还是 *T 类型实例,都既可以调用 receiver 为 T 类型的方法,也可以调用 receiver 为 *T 类型的方法 。这都是 Go 编译器在背后做的转换,当 Go 发现类型不一致时,会自动转换。

示例如下:

type T struct {
func (t T) M1() {
t.a = 10
func (t *T) M2() {
t.a = 11
func main() {
var t1 T
println(t1.a) // 0
t1.M1()
println(t1.a) // 0
t1.M2()
println(t1.a) // 11
var t2 = &T{}
println(t2.a) // 0
t2.M1()
println(t2.a) // 0
t2.M2()
println(t2.a) // 11

3,一个思考题

type field struct {
name string
func (p *field) print() {
fmt.Println(p.name)
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print()
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print()
time.Sleep(3 * time.Second)
// 其结果是
three
type field struct {
name string
func (p field) print() {
fmt.Println(p.name)
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 {
go v.print()
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 {
go v.print()
time.Sleep(3 * time.Second)
// 其结果是
three

4,方法集合

Go 中任何一个类型都有属于自己的方法集合,或者说方法集合是 Go 类型的一个“属性”。

方法集合是用来判断一个类型是否实现了某接口类型的唯一手段。

但不是所有类型都有自己的方法,比如 int 类型就没有。所以,对于没有定义方法的 Go 类型,我们称其拥有空方法集合。

方法集合可以分两种来讨论:

  • 非接口类型

接口类型相对特殊,它只会列出代表接口的方法列表,不会具体定义某个方法,它的方法集合就是它的方法列表中的所有方法。

函数 dumpMethodSet,用于输出一个非接口类型的方法集合:

func dumpMethodSet(i interface{}) {
dynTyp := reflect.TypeOf(i)
if dynTyp == nil {
fmt.Printf("there is no dynamic type\n")
return
n := dynTyp.NumMethod()
if n == 0 {
fmt.Printf("%s's method set is empty!\n", dynTyp)
return
fmt.Printf("%s's method set:\n", dynTyp)
for j := 0; j < n; j++ {
fmt.Println("-", dynTyp.Method(j).Name)
fmt.Printf("\n")

再看下面代码:

type T struct{}
func (T) M1() {}
func (T) M2() {}
func (*T) M3() {}
func (*T) M4() {}
func main() {
var n int
dumpMethodSet(n)
dumpMethodSet(&n)
var t T
dumpMethodSet(t)
dumpMethodSet(&t)

输出如下:

int's method set is empty!
*int's method set is empty!
main.T's method set:
*main.T's method set:
- M1 // 初学者不容易理解的地方
- M2 // 初学者不容易理解的地方

Go 语言规定,*T 类型的方法集合包含所有以 *T 为 receiver 参数类型的方法,以及所有以 T 为 receiver 参数类型的方法。

方法集合决定接口实现的含义就是:如果某类型 T 的方法集合与某接口类型的方法集合相同,或者类型 T 的方法集合是接口类型 I 方法集合的超集,那么我们就说这个类型 T 实现了接口 I。或者说,方法集合这个概念在 Go 语言中的主要用途,就是用来判断某个类型是否实现了某个接口

... method has pointer receiver 问题

type QuackableAnimal interface {
Quack()
type Duck struct{}
// d 的类型是 Duck
func (d Duck) Quack() {
func AnimalQuackInForest(a QuackableAnimal) {
func main() {
AnimalQuackInForest(Duck{}) // 传 T 类型没问题
AnimalQuackInForest(&Duck{}) // 传 *T 类型没问题

查看方法集:

dumpMethodSet(Duck{})
dumpMethodSet(&Duck{})
// 输出如下
main.Duck's method set:
- Quack
*main.Duck's method set:
- Quack

再看代码:

type QuackableAnimal interface {
Quack()
type Duck struct{}
// d 的类型是 *Duck
func (d *Duck) Quack() {
func AnimalQuackInForest(a QuackableAnimal) {
func main() {
AnimalQuackInForest(Duck{}) // 传 T 类型有问题,编译异常:Quack method has pointer receiver
AnimalQuackInForest(&Duck{}) // 传 *T 类型没问题

此时 AnimalQuackInForest(Duck{}) 出问题的原因是,Duck 类型没有实现 QuackableAnimal 接口。

通过 dumpMethodSet(Duck{}) 查看 Duck 的方法集,可知 Duck 的方法集为空。

dumpMethodSet(Duck{})
dumpMethodSet(&Duck{})
// 输出如下
main.Duck's method set is empty!
*main.Duck's method set:
- Quack

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK