百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

golang 方法和方法集

ztj100 2025-01-10 18:39 18 浏览 0 评论

方法

概述: 在面向对象编程, 一个对象其实也就是一个简单的值或者一个变量, 在这个对象中会包含一些函数, 这种带有接收者的函数, 我们称为为方法(method)。

本质上, 一个方法则是一个和特殊类型关联的函数。

一个面向对象的程序会用方法来表达其属性和对应的操作, 这样使用这个对象的用户就不需要直接去操作对象, 而是借助方法来做这些事情。

在Go语言中, 可以给任意自定义类型(包括内置类型, 但不包括指针类型)添加相应的方法。

方法总是绑定对象实例, 并隐式将实例作为第一实参(receiver), 方法的语法如下:

func (receiver ReceiverType) funcName(paramters) (results)

参数receiver可任意命名。 如方法中未曾使用, 可省略参数名。 receiver: 接收者 [r??si?v?(r)]

参数receiver类型可以是 T 或 *T。 基类型 T 不能是接口或指针

不支持重载方法, 也就是说, 不能定义名字相同但是不同参数的方法。

注意:

每个方法只能有一个 receiver 参数, 不支持多 receiver 参数列表或变长 receiver 参数。

一个方法只能绑定一个基类型, Go 语言不支持同时绑定多个类型的方法。

receiver 参数的基类型本身不能是指针类型或接口类型。

如: 下面的 receiver参数是无效的

type MyInt *int
func (r MyInt) String() string { // invalid receiver type MyInt (MyInt is a pointer type)
    return fmt.Sprintf("%d", *(*int)(r))
}
type MyReader io.Reader
func (r MyReader) Read(p []byte) (int, error) { // invalid receiver type MyReader (MyReader is an interface type)
    return r.Read(p)
}

Go 方法具有如下特点:

方法名的首字母是否大写决定了该方法是否是导出方法 ;

方法定义要与类型定义放在同一个包内。

由于方法定义与类型定义必须放在同一个包下面, 因此我们可以推论得到:我们不能为原生类型(诸如:int、float64、map 等)添加方法, 只能为自定义类型定义方法。

错误的作法:

func (i int) String() string { // cannot define new methods on non-local type int
    return fmt.Sprintf("%d", i)
}
正确的作法:
type MyInt int
func (i MyInt) String() string {
    return fmt.Sprintf("%d", int(i))
}

常见的问题: cannot define new methods on non-local type int

原因: go语言不允许为简单的内置类型添加方法, 一般用于自定义结构体 struct, 其他数据类型不推荐使用;

为类型添加方法

基础类型作为接收者

package main //必须有个main包
import "fmt"
type MyInt int //自定义类型, 给int改名为MyInt, 定义方法必须使用自定义类型或者结构体
//在函数定义时, 在其名字之前放上一个变量, 即是一个方法
func (a MyInt) Add(b MyInt) MyInt { //面向对象
    return a + b
}
//传统方法定义
func Add(a, b MyInt) MyInt {
    return a + b
}
func main(){
    var a MyInt = 1
    var b MyInt = 1

    //调用func (a MyInt) Add(b MyInt)
    fmt.Println("a.Add(b) = ", a.Add(b)) //a.Add(b) == 2

    //调用func Add(a, b MyInt)
    fmt.Println("Add(a, b) = ", Add(a, b)) //Add(a, b) = 2
}

通过上面的例子可以看出, 面向对象只是换了一种语法形式来表达。方法是函数的语法糖, 因为receiver其实就是方法所接收的第1个参数。

注意: 虽然方法的名字一模一样, 但是如果接收者不一样, 那么方法就不一样。

package main //必须有个main包
import "fmt"
type MyInt int //自定义类型, 给int改名为MyInt, 定义方法必须使用自定义类型或者结构体
//在函数定义时, 在其名字之前放上一个变量, 即是一个方法
func (a MyInt) Add(b MyInt, c MyInt) MyInt { //面向对象 参数可以多出一个, 不会报bug
    return a + b
}
//传统方法定义
func Add(a, b, c MyInt) MyInt { // 参数可以多出一个, 不会报bug
    return a + b
}
func main() {
    var a MyInt = 1
    var b MyInt = 1
    //调用func (a MyInt) Add(b MyInt)
    fmt.Println("a.Add(b) = ", a.Add(b, 10)) //a.Add(b) == 2
    //调用func Add(a, b MyInt)
    fmt.Println("Add(a, b) = ", Add(a, b, 10)) //Add(a, b) = 2
}

注意: 函数和方法多出的参数没有使用, 不会报bug(如: 参数c);

结构体作为接收者

方法里面可以访问接收者的字段, 调用方法通过点(.)访问, 就像struct里面访问字段一样:

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
func (p Person) PrintInfo(){
    fmt.Println(p.name, p.sex, p.age)
}
func main(){
    p := Person{"mike", 'm', 18} //初始化
    p.PrintInfo() //调用func (p Person) PrintInfo()
}

输出结果:

mike 109 18

值语义和引用语义

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//指针作为接收者, 引用语义
func (p *Person) SetInfoPointer(){
    //给成员赋值
    (*p).name = "yoyo"
    p.sex = 'f'
    p.age = 22
}
//值作为接收者, 值语义
func (p Person) SetInfoValue(){
    //给成员赋值
    p.name = "yoyo"
    p.sex = 'f'
    p.age = 22
}
func main(){
    //指针作为接收者, 引用语义
    p1 := Person{"mike", 'm', 18} //初始化
    fmt.Println("函数调用前 = ", p1) //函数调用前 = {mike 109 18}

    p1.SetInfoPointer() // 这里没有使用指针不会改变接收者(receiver)是引用语义, p 会转换成 &p
    fmt.Println("函数调用后 = ", p1) //函数调用后 = {yoyo 102 22}
    (&p1).SetInfoPointer()
    fmt.Println("函数调用后 = ", p1) //函数调用后 = {yoyo 102 22}

    fmt.Println("==========")

    //值类型作为接收者, 值语义
    p2 := Person{"mike", 'm', 18} //初始化
    fmt.Println("函数调用前 = ", p2) //函数调用前 = {mike 109 18}

    p2.SetInfoValue()
    fmt.Println("函数调用后 = ", p2) //函数调用后 = {mike 109 18} 值语义不会改变原有的值

    (&p2).SetInfoValue() // 这里的 "&" 不会改变接收者(receiver)是值语义, &p 会转换成 p
    fmt.Println("函数调用后 = ", p2) //函数调用后 = {mike 109 18} 值语义不会改变原有的值
}

由此可见: 决定值语义还是引用语义, 在于定义方法是否作为指针作为接收者;

接收者(receiver)究竟是指针(引用语义)还是值(值语义)类型, 是在定义时候决定, 而不是在使用的时候决定;

值语义和引用语义的区别:

Go 语言中的大多数类型都是值语义。值语义和引用的区别在于赋值之后, 重新赋值, 是否会改变原值。

如果不改变原值, 则是值语义。否则是引用语义, 引用语义比值语义拥有更复杂的存储结构。比如分配内存、指针、长度、容量等。

总结:

值接收者(值语义) vs 指针接收者(引用语义)

要改变内容必须使用指针接收者

结构过大也考虑使用指针接收者

一致性: 如有指针接收者, 最好都是指针接收者

值接收者是go语言特有

值/指针接收者均可接收值/指针

方法集

类型的方法集是指可以被该类型的值调用的所有方法的集合。

用实例 value 和 pointer 调用方法(含匿名字段)不受方法集约束, 编译器总是查找全部方法, 并自动转换 receiver 实参。

类型 *T 方法集

一个指向自定义类型的值的指针, 它的方法集由该类型定义的所有方法组成, 无论这些方法接受的是一个值还是一个指针。

如果在指针上调用一个接受值的方法, Go语言会聪明地将该指针解引用, 并将指针所指的底层值作为方法的接收者。

类型 *T 方法法集包含全部 receiver T + *T 方法:

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//指针作为接收者, 引用语义
func (p *Person) SetInfoPointer() {
    (*p).name = "yoyo"
    p.sex = 'f'
    p.age = 22
    fmt.Println("SetInfoPointer")
}
//值作为接收者, 值语义
func (p Person) SetInfoValue(){
    p.name = "xxx"
    p.sex = 'm'
    p.age = 33
    fmt.Println("SetInfoValue")
}
func main(){
    //p为指针类型
    var p *Person = &Person{"mike", 'm', 18} // 此时的"*"代表指针类型
    p.SetInfoPointer() //SetInfoPointer 内部将p转化为*p, 再调用
    (*p).SetInfoPointer() //SetInfoPointer 等同于 p.SetInfoPointer(), 但是效率高, 此时的"*"代表操作符, 因为 p 传递的是指针数据类型

    p.SetInfoValue() //SetInfoValue 同于 (*p).SetInfoValue(), 但是效率高
    (*p).SetInfoValue() //SetInfoValue 内部将*p转化为p, 再调用
}

类型 T 方法集

一个自定义类型值的方法集则由为该类型定义的接收者类型为值类型的方法组成, 但是不包含那些接收者类型为指针的方法。

但这种限制通常并不像这里所说的那样, 因为如果我们只有一个值, 仍然可以调用一个接收者为指针类型的方法, 这可以借助于Go语言传值的地址能力实现。

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//指针作为接收者, 引用语义
func (p *Person) SetInfoPointer() {
    (*p).name = "yoyo" // * 代表操作符
    p.sex = 'f'
    p.age = 22
    fmt.Println("SetInfoPointer")
}
//值作为接收者, 值语义
func (p Person) SetInfoValue(){
    p.name = "xxx"
    p.sex = 'm'
    p.age = 33
    fmt.Println("SetInfoValue")
}
func main(){
    //p为普通类型
    p := Person{"mike", 'm', 18}
    (&p).SetInfoPointer() //SetInfoPointer //代表传址
    (p).SetInfoPointer() //SetInfoPointer 内部先把p转化为&p后, 再调用

    p.SetInfoValue() //SetInfoValue
    (&p).SetInfoValue() //SetInfoValue 内部先把&p转化为p后, 再调用
    //(*p).SetInfoPointer() //err, invalid indirect of s (type Person)
    //(*s).SetInfoValue() //err, invalid indirect of s (type Person)
    fmt.Println(p.name, p.age, p.sex) //yoyo 22 102
}

匿名字段

方法的继承

如果匿名字段实现了一个方法, 那么包含这个匿名字段的struct也能调用这个方法

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//Person定义了方法
func (p *Person) PrintInfo() {
    fmt.Printf("%s, %c, %d\n", p.name, p.sex, p.age)
}
type Student struct {
    Person //匿名字段, 那么 Student 包含了 Person 的所有字段
    id int
    addr string
}
func main(){
    p := Person{"mike", 'm', 18}
    p.PrintInfo() //mike, m, 18

    s := Student{Person{"yoyo", 'f', 20}, 2, "sz"}
    s.PrintInfo() //yoyo, f, 20
}

方法的重写

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//Person定义了方法
func (p *Person) PrintInfo() {
    fmt.Printf("Person: %s, %c, %d\n", p.name, p.sex, p.age)
}
type Student struct {
    Person //匿名字段, 那么 Student 包含了 Person 的所有字段\
    id int
    addr string
}
func (s *Student) PrintInfo() {
    fmt.Printf("Student: %s, %c, %d\n", s.name, s.sex, s.age)
}
func main(){
    p := Person{"mike", 'm', 18}
    p.PrintInfo() //Person: mike, m, 18

    s := Student{Person{"yoyo", 'f', 20}, 2, "sz"}
    s.PrintInfo() //Student: yoyo, f, 20
    s.Person.PrintInfo() //Person: yoyo, f, 20
}

表达式

类似于我们可以对函数进行赋值和传递一样, 方法也可以进行赋值和传递。

根据调用者不同, 方法分为两种表现形式, 方法值和方法表达式。 两者都可像普通函数那样赋值和传参, 区别在于方法值绑定实例, 而方法表达式则须显式传参。

方法值

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//Person定义了方法
func (p *Person) PrintInfoPointer() {
    fmt.Printf("%p, %v\n", p, p)
}
func (p Person) PrintInfoValue() {
    fmt.Printf("%p, %v\n", &p, p)
}
func main(){
    p := Person{"mike", 'm', 18}
    p.PrintInfoPointer() //0xc0000640c0, &{mike 109 18}

    pFunc1 := p.PrintInfoPointer //方法值, 隐式传递receiver
    pFunc1() //0xc0000640c0, &{mike 109 18}

    pFunc2 := p.PrintInfoValue
    pFunc2() //0xc000064140, {mike 109 18}
}

方法表达式

package main //必须有个main包
import "fmt"
type Person struct {
    name string
    sex byte
    age int
}
//Person定义了方法
func (p *Person) PrintInfoPointer() {
    fmt.Printf("%p, %v\n", p, p)
}
func (p Person) PrintInfoValue() {
    fmt.Printf("%p, %v\n", &p, p)
}
func main(){
    p := Person{"mike", 'm', 18}
    p.PrintInfoPointer() //0xc0000640c0, &{mike 109 18}

    //方法表达式, 须显式传参
    pFunc1 := (*Person).PrintInfoPointer
    pFunc1(&p) //0xc000004460, &{mike 109 18}

    pFunc2 := Person.PrintInfoValue
    pFunc2(p) //0xc0000044e0, {mike 109 18}
}

总结:

结构体的方法集和非结构体的方法集区别

结构体的方法集

package main
import (
    "fmt"
)
type T struct {
    int
}
func (t T) test() {
    fmt.Println("类型 T 方法集包含全部 receiver T 方法。")
}
func main() {
    t1 := T{1}
    fmt.Printf("t1 is : %v\n", t1)
    t1.test()
}

非结构体的方法集

package main
import (
"fmt"
)
type T int
func (t T) test() {
    fmt.Println("类型 T 方法集包含全部 receiver T 方法。")
}
func main() {
    var t1 T
    t1 = 10
    fmt.Printf("t1 is : %v\n", t1)
    t1.test()
}

隐式传递和显示传递区别

package main
import "fmt"
type User struct {
    id int
    name string
}
func (self *User) Test() {
    fmt.Printf("%p, %v\n", self, self)
}
func main() {
    u := User{1, "Tom"}
    u.Test()
    mValue := u.Test
    mValue() // 隐式传递 receiver
    mExpression := (*User).Test
    mExpression(&u) // 显式传递 receiver
}

立即复制 receiver, 因为不是指针类型, 不受后续修改影响。

package main
import "fmt"
type User struct {
    id int
    name string
}
func (self User) Test() {
    fmt.Println(self)
}
func main() {
    u := User{1, "Tom"}
    mValue := u.Test // 立即复制 receiver,因为不是指针类型,不受后续修改影响。
    u.id, u.name = 2, "Jack"
    u.Test()
    mValue()
}

Go 方法的本质:一个以方法所绑定类型实例为第一个参数的普通函数;

Go 语法甜头使得我们通过类型实例调用类型方法时无需考虑实例类型与 receiver 参数类型是否一致, 编译器会为我们做自动转换;

receiver 参数类型选择时要看是否要对类型实例进行修改; 如有修改需求, 则选择*T; 如无修改需求, T 类型 receiver 传值的性能损耗也是考量因素之一。

相关推荐

其实TensorFlow真的很水无非就这30篇熬夜练

好的!以下是TensorFlow需要掌握的核心内容,用列表形式呈现,简洁清晰(含表情符号,<300字):1.基础概念与环境TensorFlow架构(计算图、会话->EagerE...

交叉验证和超参数调整:如何优化你的机器学习模型

准确预测Fitbit的睡眠得分在本文的前两部分中,我获取了Fitbit的睡眠数据并对其进行预处理,将这些数据分为训练集、验证集和测试集,除此之外,我还训练了三种不同的机器学习模型并比较了它们的性能。在...

机器学习交叉验证全指南:原理、类型与实战技巧

机器学习模型常常需要大量数据,但它们如何与实时新数据协同工作也同样关键。交叉验证是一种通过将数据集分成若干部分、在部分数据上训练模型、在其余数据上测试模型的方法,用来检验模型的表现。这有助于发现过拟合...

深度学习中的类别激活热图可视化

作者:ValentinaAlto编译:ronghuaiyang导读使用Keras实现图像分类中的激活热图的可视化,帮助更有针对性...

超强,必会的机器学习评估指标

大侠幸会,在下全网同名[算法金]0基础转AI上岸,多个算法赛Top[日更万日,让更多人享受智能乐趣]构建机器学习模型的关键步骤是检查其性能,这是通过使用验证指标来完成的。选择正确的验证指...

机器学习入门教程-第六课:监督学习与非监督学习

1.回顾与引入上节课我们谈到了机器学习的一些实战技巧,比如如何处理数据、选择模型以及调整参数。今天,我们将更深入地探讨机器学习的两大类:监督学习和非监督学习。2.监督学习监督学习就像是有老师的教学...

Python教程(三十八):机器学习基础

...

Python 模型部署不用愁!容器化实战,5 分钟搞定环境配置

你是不是也遇到过这种糟心事:花了好几天训练出的Python模型,在自己电脑上跑得顺顺当当,一放到服务器就各种报错。要么是Python版本不对,要么是依赖库冲突,折腾半天还是用不了。别再喊“我...

超全面讲透一个算法模型,高斯核!!

...

神经网络与传统统计方法的简单对比

传统的统计方法如...

AI 基础知识从0.1到0.2——用“房价预测”入门机器学习全流程

...

自回归滞后模型进行多变量时间序列预测

下图显示了关于不同类型葡萄酒销量的月度多元时间序列。每种葡萄酒类型都是时间序列中的一个变量。假设要预测其中一个变量。比如,sparklingwine。如何建立一个模型来进行预测呢?一种常见的方...

苹果AI策略:慢哲学——科技行业的“长期主义”试金石

苹果AI策略的深度原创分析,结合技术伦理、商业逻辑与行业博弈,揭示其“慢哲学”背后的战略智慧:一、反常之举:AI狂潮中的“逆行者”当科技巨头深陷AI军备竞赛,苹果的克制显得格格不入:功能延期:App...

时间序列预测全攻略,6大模型代码实操

如果你对数据分析感兴趣,希望学习更多的方法论,希望听听经验分享,欢迎移步宝藏公众号...

AI 基础知识从 0.4 到 0.5—— 计算机视觉之光 CNN

...

取消回复欢迎 发表评论: