go——类型的本质
在声明一个新类型之后,声明一个该类型的方法之前,需要先回答一个问题:这个类型的本质是什么。
如果给这个类型增加或删除某个值,是要创建一个新值,还是要更改当前的值?
如果是要创建一个新值,该类型的方法就使用值接收者。如果是要修改当前值,就使用指针接收者。
这个答案也会影响程序内部传递这个类型的值的方式:是按值做传递,还是按指针做传递。保持传递的一致性很重要。
这个背后的原则是,不要只关注某个方法是如何处理这个值的,而是要关注这个值的本质是什么?
1.内置类型
内置类型是由语言提供的一组类型。我们已经见过这些类型,分别是数值类型、字符串类型和布尔类型。
这些类型本质上是原始的类型。因此,当对这些值进行增加或删除的时候,会创建一个新值。
基于这个结论,当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本。
让我们看一下标准库里使用这些内置类型的值的函数。
1 2 3 4 5 6 | func Trim(s string, cutset string) string { if s == "" || cutset == "" { return s } return TrimFunc(s, makeCutsetFunc(cutset)) } |
可以看到标准库里strings包的Trim函数。Trim函数传入一个string类型的值做操作,再传入一个string类型的值用于查找。
之后函数会返回一个新的string值作为操作的结果。这个函数对调用者原始的string值得一个副本做操作,并返回一个新的string值的副本。
字符串(string)就像整数、浮点数和布尔值一样,本质上是一种很原始的数据值,所以函数或方法内外传递时,要传递字符串的一份副本。
再来看一个体现内置类型具有的原始本质的第二个例子。
1 2 3 4 5 6 7 | func isShellSpecialVar(c uint8) bool { switch c { case '*' , '#' , '$' , '@' , '!' , '?' , '-' , '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' : return true } return false } |
这个函数传入了int8类型的值,并返回一个bool类型的值。
注意,这里的参数没有使用指针来共享参数的值或者返回值。调用者传入一个uint8值得副本,并接受一个返回值true或false。
2.引用类型
Go语言里得引用类型有如下几个:切片、字典、通道、接口和函数类型。
当声明上述类型得变量时,创建的变量被称作标头(header)值。
从技术细节上说,字符串也是一种引用类型。
每个引用类型创建的标头值是包含一个指向底层数据结构的指针。
每个引用类型还包含一组独特的字段,用于管理底层数据结构。
因为标头值是为复制而设计的,所以永远不需要共享一个引用类型的值。
标头值里包含一个指针,因此通过复制来传递一个引用类型的值的副本,本质上就是在共享底层数据结构。
1 | type IP []byte |
上面代码展示了一个名为IP的类型,这个类型被声明为字节切片。
当要围绕相关的内置类型或者引用类型来声明用户定义的行为时,直接基于已有类型来声明用户定义的类型会很好用。
编译器只允许为命名的用户定义的类型声明方法。
1 2 3 4 5 6 7 8 9 | func (ip IP) MarshalText() ([]byte, error) { if len(ip) == 0 { return []byte( "" ), nil } if len(ip) != IPv4len && len(ip) != IPv6len { return nil, &AddrError{Err: "invalid IP address" , Addr: hexString(ip)} } return []byte(ip.String()), nil } |
MarshalText方法是用IP类型的值接收者声明的。
一个值接收者,正像预期的那样通过复制来传递引用,从而不需要通过指针来共享引用类型的值。
这种传递方法也可以应用到函数或者方法的参数传递。
1 2 3 4 5 6 7 8 | // ipEmptyString像ip.String一样 // 只不过在没有设置ip时会返回一个空字符串 func ipEmptyString(ip IP) string { if len(ip) == 0 { return "" } return ip.String() } |
上述代码中有一个ipEmptyString函数。这个函数需要传入一个IP类型的值。
再一次,你可以看到调用者传入的是这个引用类型的值,而不是通过引用共享给这个函数。
调用者将引用类型的值的副本传入这个函数。这种方法也适合函数的返回值。
引用类型的值在其它方面像原始的数据类型的值一样对待。
3.结构类型
结构类型可以用来描述一组数据值,这组值的本质既可以是原始的,也可以是非原始的。
如果决定在某些东西需要删除或添加某个结构类型的值时该结构类型的值不应该被更改,那么需要遵守之前提到的内置类型或引用类型的规范。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | type Time struct { // wall and ext encode the wall time seconds, wall time nanoseconds, // and optional monotonic clock reading in nanoseconds. // // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic), // a 33-bit seconds field, and a 30-bit wall time nanoseconds field. // The nanoseconds field is in the range [0, 999999999]. // If the hasMonotonic bit is 0, then the 33-bit field must be zero // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext. // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit // unsigned wall seconds since Jan 1 year 1885, and ext holds a // signed 64-bit monotonic clock reading, nanoseconds since process start. wall uint64 ext int64 // loc specifies the Location that should be used to // determine the minute, hour, month, day, and year // that correspond to this Time. // The nil location means UTC. // All UTC times are represented with loc==nil, never loc==&utcLoc. loc *Location } |
上面代码是源码中time包中一段代码。
当思考时间的值时,你应该意识到给定的一个时间点的时间是不能修改的。所以标准库里也是这样实现Time类型的。
让我们看一下Now函数是如何创建Time类型的值的。
1 2 3 4 5 6 7 8 | func Now() Time { sec, nsec, mono := now() sec += unixToInternal - minWall if uint64(sec)>>33 != 0 { return Time{uint64(nsec), sec + minWall, Local} } return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local} } |
这个函数创建了一个Time类型的值,并给调用者返回time值得副本。这个函数没有使用指针来共享Time值。
来看一下Time类型的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | func (t Time) Add(d Duration) Time { dsec := int64(d / 1e9) nsec := t.nsec() + int32(d%1e9) if nsec >= 1e9 { dsec++ nsec -= 1e9 } else if nsec < 0 { dsec-- nsec += 1e9 } t.wall = t.wall&^nsecMask | uint64(nsec) // update nsec t.addSec(dsec) if t.wall&hasMonotonic != 0 { te := t.ext + int64(d) if d < 0 && te > t.ext || d > 0 && te < t.ext { // Monotonic clock reading now out of range; degrade to wall-only. t.stripMono() } else { t.ext = te } } return t } |
这个方法使用值接收者,并返回了一个新的Time的值。
该方法操作的是调用者传入的Time值的副本,并且给调用者返回了一个方法内的Time值的副本。
至于是使用返回的值替换原来的Time值,还是创建一个新的Time变量来保存结果,是由调用者决定的事情。
大多数情况下,结构类型的本质并不是原始的,而是非原始的。
这种情况下,对这个类型的值做增加或者删除操作应该更改值本身。
当需要修改值本身时,在程序中其它地方,需要使用指针来共享这个值。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
2017-12-11 第12条:不要在for和while循环后面写else块
2017-12-11 将序列中的元素连起来——程序的思想(顺着电脑的思想)——闯关
2017-12-11 动态删除列表中的元素
2017-12-11 文件操作脚本(一)
2017-12-11 函数(1)
2017-12-11 构造器以及解构器
2017-12-11 文件传输协议FTP