Go语言实战之类型的本质

傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事。——–王小波

写在前面


  • 博文部分内容为《GO语言实战》读书笔记之一
  • 嗯,能力有限,书里讲的很多读不大懂,也不知是翻译的原因,嘻,读着很拗口
  • 比如这个类型的值做增加或者删除的操作这句
  • 我们平常可能会讲,这个类型的值做修改的操作
  • 整理一下,其实还是不太懂,理解不足请小伙伴帮忙指正
  • 主要涉及知识
    • 类型如何接收方法
    • 内置类型在方法和函数的传递
    • 引用类型在方法和函数的传递

傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事。——–王小波


类型的本质

在声明一个新类型之后,声明一个该类型的方法之前,需要先回答一个问题:这个类型的本质是什么。

如果给这个类型增加或者删除某个值,是要创建一个新值,还是要更改当前的值?

如何接收方法

如果是要创建一个新值,该类型的方法就使用值接收者。如果是要修改当前值,就使用指针接收者。

  • 当调用使用指针接收者声明的方法时,这个方法会共享调用方法时接收者所指向的值,即对于接收者来讲,始终是一个值,方法可以理解对接收者的加工,也可以说,当对接收者进行加工生产时,一般使用指针接收方法。

  • 当调用使用值接收者声明的方法时,会使用这个值的一个副本来执行,即用于消费这个接收者,不会对原有接收有影响。

需要说明的是GOlang对方法的调用者很宽松,既允许使用值,也允许使用指针来调用方法,不必严格符合接收者的类型。

1
2
3
4
5
6
7
8
9
10
// 值传递
func (u user) notify() {
fmt.Printf("Sending User Email To %s<%s>\n",
u.name,
u.email)
}
// 指针传递
func (u *user) changeEmail(email string) {
u.email = email
}

这个答案也会影响程序内部传递这个类型的值的方式:是按值做传递,还是按指针做传递

保持传递的一致性很重要 。这个背后的原则是,不要只关注某个方法是如何处理这个值,而是要关注这个值的本质是什么。

内置类型在方法和函数的传递

内置类型是由语言提供的一组类型,数值类型字符串类型布尔类型,这些类型本质上是原始的类型,因此,当对这些值进行增加或者删除的时候,会创建一个新值.即通过基本类似通过值传递的方式。

基于这个结论,当把内置类型 的值传递给方法或者函数时,应该传递一个对应值的副本,即使用值传递

1
2
3
4
5
6
func Trim(s, cutset string) string {
if s == "" || cutset == "" {
return s
}
return TrimFunc(s, makeCutsetFunc(cutset))
}

标准库strings 包的 Trim 函数,这个函数对调用者原始的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
}

env 包里的 isShellSpecialVar 函数。这个函数传入了一个 int8类型的值,并返回一个bool 类型的值,这里的参数没有使用指针来共享参数的值,调用者传入了一个uint8值的副本,接受一个返回值 true 或者 false。(go里面是支持switch的,但是python是不支持的)

引用类型在方法和函数的传递

Go 语言里的引用类型有如下几个:切片、映射、通道、接口和函数类型

当声明上述类型的变量时,创建的变量被称作标头(header)值。从技术细节上说,字符串也是一种引用类型

每个引用类型创建的标头值是包含一个指向底层数据结构的指针。每个引用类型还包含一组独特的字段,用于管理底层数据结构。因为标头值是为复制而设计的,所以永远不需要共享一个引用类型的值。类似Linux里面软链接的作用。

标头值里包含一个指针,因此通过复制来传递一个引用类型的值的副本,本质上就是在共享底层数据结构

通过已有类型声明一个用户定义类型IP

1
2
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 类型的值接收者声明的。一个值接收者,即IP对象通过复制来传递引用类型,从而不需要通过指针来共享引用类型的值。

这种传递方法也可以应用到函数或者方法的参数传递

1
2
3
4
5
6
func ipEmptyString(ip IP) string {
if len(ip) == 0 {
return ""
}
return ip.String()
}

ipEmptyString 函数。这个函数需要传入一个 IP 类型的值。 调用者传入的是这个引用类型的值,而不是通过引用共享给这个函数 ,这里和方法有着本质的区别,调用者将引用类型的值的副本传入这个函数。

这种方法也适用于函数的返回值。最后要说的是,引用类型的值在其他方面像原始的数据类型的值一样对待。

结构类型(用户定义类型)

结构类型可以用来描述一组数据值,这组值的本质即可以是原始的,也可以是非原始的

原始的情况

如果决定修改某个结构类型的值时,该结构类型的值不应该被更改,需要遵守之前提到的内置类型引用类型的规范。

简单来讲。对于一些结构体的值。可以看做是find的,不可被修改,只能创建返回一个新值, 所有使用值接收行为,我们看一个Time结构体的Demo

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 函数的实现

1
2
3
4
5
6
7
8
9
10
11
//func now() (sec int64, nsec int32, mono int64)
func Now() Time {
sec, nsec, mono := now()
mono -= startNano
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 变量来保存结果,是由调用者决定的事情。

非原始的情况

大多数情况下,结构类型的本质并不是原始的,而是非原始的。这种情况下,对这个类型的值做修改操作应该更改值本身。

当需要修改值本身时,在程序中其他地方,需要使用指针来共享这个值。让我们看一个由标准库中实现的具有非原始本质的结构类型的例子

\Go\src\os\types.go

1
2
3
4
//D:\Go\src\os\types.go
type File struct {
*file // os specific
}

\Go\src\os\file_windows.go

1
2
3
4
5
6
7
8
9
10
11

//file 是*File 的实际表示
// 额外的一层结构保证没有哪个系统的客户端
// 能够覆盖这些数据。如果覆盖这些数据,
// 可能在变量终结时关闭错误的文件描述符
type file struct {
pfd poll.FD
name string
dirinfo *dirInfo // 除了目录结构,此字段为 nil
appendMode bool // whether file is opened for appending
}

标准库中声明的 File 类型。这个类型的本质是非原始的,这个类型的值实际上不能安全复制(可以理解为没有读锁)。因为没有方法阻止程序员进行复制,所以File类型的实现使用了一个嵌入的指针,指向一个未公开的类型.正是这层额外的内嵌类型阻止了复制。

我理解通过指针内嵌的方式,对File私有化,在多重读写中,保证了文件不被覆盖。

不是所有的结构类型都需要或者应该实现类似的额外保护。程序员需要能识别出每个类型的本质,并使用这个本质来决定如何组织类型。

Open 函数的实现

1
2
3
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}

调用者得到的是一个指向 File 类型值的指针。Open 创建了 File 类型的值,并返回指向这个值的指针。如果一个创建用的工厂函数返回了一个指针,就表示这个被返回的值的本质是非原始的。

即便函数或者方法没有直接改变非原始的值的状态,依旧应该使用共享的方式传递.

即使没有修改接收者的值,依然是用指针接收者来声明的。因为 File 类型的值具备非原始的本质,所以总是应该被共享,而不是被复制。

是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质。

这条规则的一个例外是,需要让类型值符合某个接口的时候,即便类型的本质是非原始本质的,也可以选择使用值接收者声明方法。这样做完全符合接口值调用方法的机制。

这部分还是有些不太明白,之后有时间还需要在看看。

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知,这是一个开源项目,如果你认可它,不要吝啬星星哦 :)


《GO语言实战》


© 2018-2023 liruilonger@gmail.com, All rights reserved. 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

发布于

2022-04-14

更新于

2023-09-05

许可协议

评论
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×