Go 基础 - 记录一些 Go 的特殊语法(二)

前言
本篇接着上一篇继续分享,记录 Go 中的特殊语法。
Go 的特殊语法
选择器
上篇末尾提到结构体中可以用 x.f
来代替 (*x).f
,实际上这里的 f
被称为选择器(selector)。
对于不为包名的主表达式 x
, 选择者表达式 x.f
表示值 x
或 *x
的字段或方法 f
。 标识符 f
称为(字段或方法)选择器。
选择器会自动解引用指向结构的指针。 若 x
为指向结构体的指针,x.y
即为 (*x).y
的缩写; 若字段 y
亦为指向结构的指针,x.y.z
即为 (*(*x).y).z
的缩写, 以此类推。 若 x
包含类型为 *A
的匿名字段,且 A
为一个包含字段 f
的结构类型, x.f
即为 (*x.A).f
的缩写。
遍历到 f
的嵌入字段的数目称为它在 T
中的深度。例如, f
被直接声明在 T
中,则深度为 0; 若被声明在 T
中声明的匿名字段 A
中,则深度为 1;以此类推。
需要注意的是:
-
嵌套引用时,所嵌套的字段必须为匿名字段。
-
对于非接口类型
T
或*T
的值x
,x.f
中的f
表示在T
中最浅深度的字段或方法。 -
同一深度中(即使是不同的匿名字段)不能出现重名的字段或方法。(这会导致歧义,无法判断要调用哪一个)
例如,给定声明:
type T0 struct {
x int
}
func (*T0) M0()
type T1 struct {
y int
}
func (T1) M1()
type T2 struct {
z int
T1
*T0
}
func (*T2) M2()
var t = T2{T0: &T0{}} // with t.T0 != nil
var p = &T2{T0: &T0{}} // 其中 p != nil 且 p.T0 != nil
type Q *T2
var q Q = p
可以向下面这样引用:
t.z // t.z
t.y // t.T1.y
t.x // (*t.T0).x
p.z // (*p).z
p.y // ((*p).T1).y
p.x // (*(*p).T0).x
q.z // (*q).z
q.y // ((*q).T1).y
q.x // (*(*q).T0).x
p.M2() // (*p).M2()
p.M1() // ((*p).T1).M1()
p.M0() // ((*p).T0).M0()
t.M2() // (&t).M2()
但是不可以:
q.M2()
q.M1()
q.M0()
这里涉及到类型声明的一些细节:
-
类型声明将标识符和类型名绑定至一个与现存类型有相同的基本类型的新类型;
-
新类型不继承任何绑定到现存类型的方法, 但接口类型或复合类型元素的方法集保持不变;
所以,这里的类型 Q
实际上只能选择到到基础类型,而没有继承 M2() M1() M0()
这些方法的。
在这种情况下,如果想要调用上面这些方法,需写为 (*q).M0()
,不过这就不是一个选择器了
函数
当两个或多个连续命名的函数参数为同一个类型时,可以在除最后一个参数之外的所有参数中省略该类型。
func f(x, y int, s bool) int {
if s {
return x + y
} else {
return x - y
}
}
函数可以返回多个值,接收时需用同等数量的变量来接收,或使用 _
来舍弃某些值。
函数的返回值可以被命名,被命名的返回值将被视为定义在函数顶部的变量,此时可直接使用不带参数的 return
。
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
func main() {
var x, y int
x, _ = split(17)
fmt.Println(x, y)
}
参数传递实际上都是值传递,指针也是对内存地址的拷贝。
函数可以返回一个局部变量的地址,该局部变量对应的数据在函数返回后依然有效。
我们通过下面的示例来说明上面两条性质:
package main
func split(sum *int) (*int, *int) {
println(&sum, sum)
x := *sum * 4 / 9
y := *sum - x
println(&x, &y)
return &x, &y
}
func main() {
var x, y *int
println(&x, &y)
sum := 17
println(&sum)
x, y = split(&sum)
println(&x, &y, x, y, *x, *y)
}
运行结果如下:
0xc00005bf60 0xc00005bf58
0xc00005bf50
0xc00005bf68 0xc00005bf50
0xc00005bf48 0xc00005bf40
0xc00005bf60 0xc00005bf58 0xc00005bf48 0xc00005bf40 7 10
第三行可以看出来,main
中的 sum
和 split
中的 sum
所在地址是不一样的,但是后者保存了(指向)前者的地址;从最后一行可以看出来,main
中的 x
、 y
指向 split
中两个局部变量的地址,并且是可以访问的。
数组与切片
数组
在Go中:
- 数组是值。将一个数组赋给另一个数组会复制其所有元素。
- 当将某个数组传入某个函数时,函数将接收到该数组的一份副本而非指针。
- 数组的大小是其类型的一部分。类型
[10]int
和[20]int
是不同的。
使用数组可以做一些更细节的内存规划,避免过多的内存分配,但是数组更普遍的用途还是给切片提供存储空间。
切片
切片可以理解为是对底层数组的引用,在赋值或传递参数时,切片引用的是同一个数组。
-
实际上,切片是一个值,它本身只是对一个数组片段的描述,它包括指向数组的指针、片段的长度和容量,所以在传递参数时,传递的是这些描述的副本。
-
构造高维的切片时,内部的切片必须单独地通过
make
来进行分配。 -
当使用
append
向切片追加元素,超过切片的容量(底层数组的大小)时,会分配一个新的,足够大的切片(以及底层数组)。
这里做了一个比较有意思的实验(在查看运行结果之前,你可以先猜一猜结果是什么~):
package main
import "fmt"
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
a := names[0:2]
b := names[1:3]
fmt.Println(a, b, names)
a = append(a, "aaa")
a = append(a, "bbb")
fmt.Println(a, b, names)
b = append(b, "ddd")
fmt.Println(a, b, names)
a = append(a, "ccc")
fmt.Println(a, b, names)
a = a[:1]
a = append(a, "ttt")
fmt.Println(a, b, names)
}
我们向数组的一部分切片中往后追加元素,甚至是超过原数组的大小,看看会发生什么。
运行结果如下:
[John Paul] [Paul George] [John Paul George Ringo]
[John Paul aaa bbb] [Paul aaa] [John Paul aaa bbb]
[John Paul aaa ddd] [Paul aaa ddd] [John Paul aaa ddd]
[John Paul aaa ddd ccc] [Paul aaa ddd] [John Paul aaa ddd]
[John ttt] [Paul aaa ddd] [John Paul aaa ddd]
实际上,这个运行结果是符合我们对于切片底层数组分配的认识的。一开始, a, b
两个切片共享 names
数组,向 a
追加元素 "aaa"
和 "bbb"
,这时还没超过 names
的大小,所以会修改 names
的元素,同时在 b
中也可以观察到。同理,向 b
追加元素 "ddd"
时,也会修改 names
。当 a
追加元素 "ccc"
时,已经超过了 names
的大小,它被分配了一个新的底层数组,所以修改不会在 b
和 names
中反映出来。最后,将 a
大小变回 1,再追加 "ttt"
,这时已经不会影响到 names
和 b
了。
“陷阱”
当数组被保存在内存中时,任何对它的引用都会使它不能被 GC 释放,又因为切片操作不会修改或复制底层数组,所以有时候会出现因为一个小的引用而导致一块很大的内存被一直占用的问题。
例如:
var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte { // 几个字节的引用导致整个文件一直被存储在内存中
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
func FindDigits(filename string) []byte { // 分配一个新内存给需要的数据
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, 0, len(b))
return append(c, b...)
}
总结
Go 作为一门性能与灵活兼顾的语言,它的语法中还是会有许多的小细节需要我们学习和理解。一方面,这些细节可以提升我们的编程效率;另一方面,你在未来编程的过程中遇到的一些 bug 很有可能就是因为语法中这些特殊的地方。所以,熟读文档还是很有必要的。
最后,如果本篇文章对你有所帮助,希望你可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿
转载自:https://juejin.cn/post/7129163809410777101