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

golang 方法和方法集

ztj100 2025-01-10 18:39 12 浏览 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 传值的性能损耗也是考量因素之一。

相关推荐

干货 | 各大船公司VGM提交流程(msc船运公司提单查询)

VGM(VerifiedGrossMass)要来了,大外总管一本正经来给大家分享下各大船公司提交VGM流程。1,赫伯罗特(简称HPL)首先要注册账户第一,登录进入—选择product------...

如何修改图片详细信息?分享三个简单方法

如何修改图片详细信息?分享三个简单方法我们知道图片的详细信息里面包含了很多属性,有图片的创建时间,修改时间,地理位置,拍摄时间,还有图片的描述等信息。有时候为了一些特殊场景的需要我们需要对这些信息进行...

实用方法分享:没有图像处理软件,怎么将一张照片做成九宫格?

在发朋友圈时,如果把自己的照片做成九宫格,是不是更显得高大上?可能你问,是不是要借助图片处理软件,在这里,我肯定告诉你,不需要!!!你可能要问,那怎么实现呢?下面你看我是怎么做的,一句代码都不写,只是...

扫描档PDF也能变身“最强大脑”?RAG技术解锁尘封的知识宝藏!

尊敬的诸位!我是一名物联网工程师。关注我,持续分享最新物联网与AI资讯和开发实战。期望与您携手探寻物联网与AI的无尽可能。今天有网友问我扫描档的PDF文件能否做知识库,其实和普通pdf处理起来差异...

这两个Python库,轻而易举就能实现MP4与GIF格式互转,太好用了

mp4转gif的原理其实很简单,就是将mp4文件的帧读出来,然后合并成一张gif图。用cv2和PIL这两个库就可以轻松搞定。importglobimportcv2fromPILimpo...

python图片处理之图片切割(python把图片切割成固定大小的子图)

python图片切割在很多项目中都会用到,比如验证码的识别、目标检测、定点切割等,本文给大家带来python的两种切割方式:fromPILimportImage"""...

python+selenium+pytesseract识别图片验证码

一、selenium截取验证码#私信小编01即可获取大量Python学习资源#私信小编01即可获取大量Python学习资源#私信小编01即可获取大量Python学习资源importjso...

如何使用python裁剪图片?(python图片截取)

如何使用python裁剪图片如上图所示,这是一张包含了各类象棋棋子的图片。我们需要将其中每一个棋子都裁剪出来,此时可以利用python的...

Python rembg 库去除图片背景(python 删除图片)

rembg是一个强大的Python库,用于自动去除图片背景。它基于深度学习模型(如U^2-Net),能够高效地将前景物体从背景中分离,生成透明背景的PNG图像。本教程将带你从安装到实际应用...

「python脚本」批量修改图片尺寸&视频安帧提取

【python脚本】批量修改图片尺寸#-*-coding:utf-8-*-"""CreatedonThuAug2316:06:352018@autho...

有趣的EXCEL&vba作图(vba画图表)

还记不记得之前有个日本老爷爷用EXCEL绘图,美轮美奂,可谓是心思巧妙。我是没有那样的艺术细胞,不过咱有自己的方式,用代码作图通过vba代码将指定的图片写入excel工作表中,可不是插入图片哦解题思...

怎么做到的?用python制作九宫格图片,太棒了

1.应用场景当初的想法是:想把一张图切割成九等份,发布到微信朋友圈,切割出来的图片,上传到朋友圈,发现微信不按照我排列的序号来排版。这样的结果是很耗时间的。让我深思,能不能有一种,直接拼接成一张...

Python-连续图片合成视频(python多张图叠加为一张)

前言很多时候,我们需要将图片直接转成视频。下面介绍用python中的OpenCV将进行多张图合成视频。cv2安装不要直接用pipinstallcv2,这会报错。有很多人建议用打开window自带的...

如何把多个文件夹里的图片提取出来?文件夹整理合并工具

在项目管理中,团队成员可能会将项目相关的图片资料分散存储在不同的文件夹中,以便于分类和阶段性管理。然而,当项目进入汇报或总结阶段时,需要将所有相关图片整合到一个位置,以便于制作演示文稿、报告或进行项目...

超简单!为图片和 PDF 上去掉水印(pdf图片和水印是一体,怎么去除)

作者:某某白米饭...

取消回复欢迎 发表评论: