百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

golang 方法和方法集

bigegpt 2024-08-12 14:21 2 浏览

方法

概述: 在面向对象编程, 一个对象其实也就是一个简单的值或者一个变量, 在这个对象中会包含一些函数, 这种带有接收者的函数, 我们称为为方法(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 传值的性能损耗也是考量因素之一。

相关推荐

了解Linux目录,那你就了解了一半的Linux系统

大到公司或者社群再小到个人要利用Linux来开发产品的人实在是多如牛毛,每个人都用自己的标准来配置文件或者设置目录,那么未来的Linux则就是一团乱麻,也对管理造成许多麻烦。后来,就有所谓的FHS(F...

Linux命令,这些操作要注意!(linux命令?)

刚玩Linux的人总觉得自己在演黑客电影,直到手滑输错命令把公司服务器删库,这才发现命令行根本不是随便乱用的,而是“生死簿”。今天直接上干货,告诉你哪些命令用好了封神!喜欢的一键三连,谢谢观众老爷!!...

Linux 命令速查手册:这 30 个高频指令,拯救 90% 的运维小白!

在Linux系统的世界里,命令行是强大的武器。对于运维小白而言,掌握一些高频使用的Linux命令,能极大提升工作效率,轻松应对各种系统管理任务。今天,就为大家奉上精心整理的30个Linu...

linux必学的60个命令(linux必学的20个命令)

以下是Linux必学的20个基础命令:1.cd:切换目录2.ls:列出文件和目录3.mkdir:创建目录4.rm:删除文件或目录5.cp:复制文件或目录6.mv:移动/重命名文件或目录7....

提高工作效率的--Linux常用命令,能够决解95%以上的问题

点击上方关注,第一时间接受干货转发,点赞,收藏,不如一次关注评论区第一条注意查看回复:Linux命令获取linux常用命令大全pdf+Linux命令行大全pdf为什么要学习Linux命令?1、因为Li...

15 个实用 Linux 命令(linux命令用法及举例)

Linux命令行是系统管理员、开发者和技术爱好者的强大工具。掌握实用命令不仅能提高效率,还能解锁Linux系统的无限潜力,本文将深入介绍15个实用Linux命令。ls-列出目录内容l...

Linux 常用命令集合(linux常用命令全集)

系统信息arch显示机器的处理器架构(1)uname-m显示机器的处理器架构(2)uname-r显示正在使用的内核版本dmidecode-q显示硬件系统部件-(SMBIOS/DM...

Linux的常用命令就是记不住,怎么办?

1.帮助命令1.1help命令#语法格式:命令--help#作用:查看某个命令的帮助信息#示例:#ls--help查看ls命令的帮助信息#netst...

Linux常用文件操作命令(linux常用文件操作命令有哪些)

ls命令在Linux维护工作中,经常使用ls这个命令,这是最基本的命令,来写几条常用的ls命令。先来查看一下使用的ls版本#ls--versionls(GNUcoreutils)8.4...

Linux 常用命令(linux常用命令)

日志排查类操作命令查看日志cat/var/log/messages、tail-fxxx.log搜索关键词grep"error"xxx.log多条件过滤`grep-E&#...

简单粗暴收藏版:Linux常用命令大汇总

号主:老杨丨11年资深网络工程师,更多网工提升干货,请关注公众号:网络工程师俱乐部下午好,我的网工朋友在Linux系统中,命令行界面(CLI)是管理员和开发人员最常用的工具之一。通过命令行,用户可...

「Linux」linux常用基本命令(linux常用基本命令和用法)

Linux中许多常用命令是必须掌握的,这里将我学linux入门时学的一些常用的基本命令分享给大家一下,希望可以帮助你们。总结送免费学习资料(包含视频、技术学习路线图谱、文档等)1、显示日期的指令:d...

Linux的常用命令就是记不住,怎么办?于是推出了这套教程

1.帮助命令1.1help命令#语法格式:命令--help#作用:查看某个命令的帮助信息#示例:#ls--help查看ls命令的帮助信息#netst...

Linux的30个常用命令汇总,运维大神必掌握技能!

以下是Linux系统中最常用的30个命令,精简版覆盖日常操作核心需求,适合快速掌握:一、文件/目录操作1.`ls`-列出目录内容`ls-l`(详细信息)|`ls-a`(显示隐藏文件)...

Linux/Unix 系统中非常常用的命令

Linux/Unix系统中非常常用的命令,它们是进行文件操作、文本处理、权限管理等任务的基础。下面是对这些命令的简要说明:**文件操作类:*****`ls`(list):**列出目录内容,显...