运维八一 运维八一
首页
运维杂记
编程浅尝
周积跬步
专栏
生活
关于
收藏
  • 分类
  • 标签
  • 归档
Source (opens new window)

运维八一

运维,运维!
首页
运维杂记
编程浅尝
周积跬步
专栏
生活
关于
收藏
  • 分类
  • 标签
  • 归档
Source (opens new window)
  • Go

    • 前言

    • Go基础知识

    • Go基本语法

    • 实战项目:简单web服务

    • 基本数据类型

    • 内置运算符

    • 分支和循环

    • 函数 function

    • 结构体 struct

    • 方法 method

      • 认识方法
      • 方法的本质
        • 2.方法的本质
      • 方法的设计
      • 用类型嵌入模拟实现“继承”
    • 实战项目:跟踪函数调用链

    • 接口 interface

    • 并发 concurrency

    • 指针

    • 实战项目:实现轻量级线程池

    • 实战项目:实现TCP服务器

    • go常用包

    • Gin框架

    • go随记

  • Python

  • Shell

  • Java

  • Vue

  • 前端

  • 编程浅尝
  • Go
  • 方法 method
lyndon
2022-06-07
目录

方法的本质

# 2.方法的本质

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

Go 的方法与 Go 中的类型是通过 receiver 联系在一起,可以为任何非内置原生类型定义方法,比如下面的类型 T:

type T struct { 
    a int
}

func (t T) Get() int {  
    return t.a 
}

func (t *T) Set(a int) int { 
    t.a = a 
    return t.a 
}

// 类型T的方法Get的等价函数
func Get(t T) int {  
    return t.a 
}

// 类型*T的方法Set的等价函数
func Set(t *T, a int) int { 
    t.a = a 
    return t.a 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这种等价转换后的函数的类型就是方法的类型。只不过在 Go 语言中,这种等价转换是由 Go 编译器在编译和生成代码时自动完成的。

方法表达式(Method Expression)

以上面类型 T 以及它的方法为例,结合前面说过的 Go 方法的调用方式,可以得到下面代码:

var t T
t.Get()
(&t).Set(1)
1
2
3

用另一种方式,把上面的方法调用做一个等价替换:

var t T
T.Get(t)
(*T).Set(&t, 1)
1
2
3

直接以类型名 T 调用方法的表达方式,被称为 Method Expression。通过 Method Expression 这种形式,类型 T 只能调用 T 的方法集合(Method Set)中的方法,同理类型 *T 也只能调用 *T 的方法集合中的方法。

Method Expression 就是 Go 方法本质的最好体现,因为方法自身的类型就是一个普通函数的类型,甚至可以将它作为右值,赋值给一个函数类型的变量,比如下面示例:

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
}
1
2
3
4
5
6
7
8
9

基于对方法本质的深入理解,来分析解决实际编码工作中遇到的真实问题:

package main

import (
    "fmt"
    "time"
)

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)

}

/*
one
two
three
six
six
six
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

问题:为什么对 data2 迭代输出的结果是三个“six”,而不是 four、five、six?

分析:

根据 Go 方法的本质,也就是一个以方法的 receiver 参数作为第一个参数的普通函数,对这个程序做个等价变换。这里利用 Method Expression 方式,等价变换后的源码如下:

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 (*field).print(v)
    }

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
        go (*field).print(&v)
    }
    
    time.Sleep(3 * time.Second)

}
// 把对 field 的方法 print 的调用,替换为 Method Expression 形式,替换前后的程序输出结果是一致的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

可以很清楚地看到使用 go 关键字启动一个新 Goroutine 时,method expression 形式的 print 函数是如何绑定参数的:

  • 迭代 data1 时,由于 data1 中的元素类型是 field 指针 (*field),因此赋值后 v 就是元素地址,与 print 的 receiver 参数类型相同,每次调用 (*field).print 函数时直接传入的 v 即可,实际上传入的也是各个 field 元素的地址;
  • 迭代 data2 时,由于 data2 中的元素类型是 field(非指针),与 print 的 receiver 参数类型不同,因此需要将其取地址后再传入 (*field).print 函数。这样每次传入的 &v 实际上是变量 v 的地址,而不是切片 data2 中各元素的地址。

一旦启动的各个子 goroutine 在 main goroutine 执行到 Sleep 时才被调度执行。

  • 前三个子 goroutine 各自传入的是元素“one”、“two”和“three”的地址,所以打印的就是“one”、“two”和“three”;
  • 最后三个 goroutine 在打印 &v 时,实际打印的也就是在 v 中存放的值“six”。

只需要*将 field 类型 print 方法的 receiver 类型由 field 改为 field ,就可以按期望输出“one”、“two”、“three”、“four”、 “five”、“six”。

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)

}

/*
one
two
three
four
five
six
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
上次更新: 2022/06/12, 15:48:09
认识方法
方法的设计

← 认识方法 方法的设计→

最近更新
01
ctr和crictl显示镜像不一致
03-13
02
alpine镜像集成常用数据库客户端
03-13
03
create-cluster
02-26
更多文章>
Theme by Vdoing | Copyright © 2015-2024 op81.com
苏ICP备18041258号-2
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式