目录
前言
- 本篇笔记基于《Go语言实战》
- 本篇笔记主要记录一些gopl里没提到的,或者不同说法的地方
- 其中也有自己测试的结论
多维数组
array := [4][2]int((10 ,11), {20, 21}, {30, 31}, {40, 41})
上图例子中,类比数学矩阵知识来说,就是创建了4行2列的矩阵(列和行都是从0开始),访问第i行第j列:array[i][j] (0<=i<=3并且0<=j<=1)
在函数传递数组是值传递
切片(slice)
使用3个索引创建切片
s := source[2:3:4]
- 计算者三个索引项:第一个值表示新切片开始的元素的索引位置,第二个值表示开始索引位置加上希望包含的元素的个数,第三个值表示开始索引位置加上希望容量中包含的个数
多维切片
s := [][]int{{10}, {100, 200}}
在函数中传递切片
- 由于切片的特殊性,它虽然也是值传递,但是传递的是指定底层数据的指针,长度,容量,不像数组那样按值拷贝
引用类型
- 切片,映射,通道,接口和函数类型都是引用类型
- 创建引用类型变量时,该变量被称为标头(header)值。
- 从技术细节上说,字符串也是一种引用类型
- 标头值包含一个指向底层数据结构的指针
标头值为复制而设计
- 笔者的话:引用类型是特殊的,特别在函数传参的时候(对于像type IP []byte声明的IP类型,它也属于引用类型,以此类推)
- 像slice和map,如果在函数中对其成员进行修改,会改变他们原始的slice或者map所指定的值,这是由于它们传参时候拷贝了一份它指向的底层数据结构的指针,也就是标头值;但是如果直接对这个传进来的参数进行修改,例如下例中的test1函数的s进行修改而不是s[1]修改,那么对原来的slice并没有什么影响(这里不贴相关例子,有兴趣的读者可以自行尝试)
package main
import (
"fmt"
)
//test slice
func test1(s []byte) {
s[1] = 57
}
//test map
func test2(m map[string]int){
m["hello"] = 1
}
//test chan
func test3(c chan int) {
c <- 3
}
//test func
func test4(a func()) {
a()
}
func main() {
s := []byte{56, 56, 56}
test1(s)
fmt.Println(s) //[56 57 56]
m := map[string]int{
"hello": 0,
}
test2(m)
fmt.Println(m) //map[hello:1]
c := make(chan int)
go test3(c)
fmt.Println(<-c) //3
f := func(){
fmt.Println("func!!!")
}
test4(f) //func!!!
}
- 换句话说,函数传参,传的始终是拷贝,但拷贝的是指针还是普通的值,这点需要区分才能更好地避免滥用
接口
两个接口的相互赋值行为例子(笔者自加)
package main
import (
"fmt"
)
type i1 interface {
func1()
}
type i2 interface {
i1
func2()
}
type s1 struct{}
func (a s1) func1() {
fmt.Println("s1 func1")
}
func (a s1) func2() {
fmt.Println("s1 func2")
}
func main() {
var a i2
a = s1{}
a.func1() //s1 func1
a.func2() //s1 func2
var b i1
b = a
b.func1() //s1 func1
b.func2() //b.func2 undefined (type i1 has no field or method func2)
}
- i2接口使用了i1接口,i1接口声明的变量b可以接收一个i2接口声明的变量作为接口值,其中原因还是接口实现的条件满足了(也就是实现了接口的方法)
- 通过这样我们可以看到,通过“接口实现接口”,接口值的方法集变小了,这也体现了组合的思想,表面看上去就像拆分了一个大接口
接口的实现(这一部分和gopl描述不完全相同)
- 如下图,接口值是一个两个字长度的数据结构
- 第一个字包含一个指向内部表iTable的指针,包含类型信息和相关的一组方法
- 第二个字是一个指向所存值的指针
- 下图与上图稍有不同,它把实体的地址交给了接口
- 第一个字iTable发生了相应的变化,而第二个字依旧保存指向实体值的指针
接口值中方法集规则
- T类型的值的方法集,只包含值接收者声明的方法;而*T类型的方法集既包含指针接收者声明的方法,也包含值接收者的方法
- 换种说法:如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能实现对应的接口;如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口
- 一个接口值中方法集规则的例子
package main
import (
"fmt"
)
type I1 interface {
func1()
}
type I2 interface {
func2()
}
type I interface {
I1
I2
}
type A struct{}
func (a A) func1() {
fmt.Println("A func1()")
}
func (a *A) func2() {
fmt.Println("*A func2()")
}
func main() {
var i1 I1
i1 = A{}
i1.func1() //A func1()
var i2 I2
//i2 = A{} 这样会出错
i2 = &A{}
i2.func2() //*A func2()
//i2.func1() 虽然用了*A类型实现fun2方法,A类型实现了fun1方法,但是接口I2只给出了func2的声明
var i I
i = &A{}
//i = A{} 这样会出错
i.func1() //A func2()
i.func2() //*A func2()
}
- 而对于非接口值里的方法,限制并没有太多,指针或非指针都能接收
package main
import (
"fmt"
)
type A struct{}
func (a A) func1() {
fmt.Println("A func1()")
}
func (a *A) func2() {
fmt.Println("*A func2()")
}
func main() {
t := A{}
t.func1() //A func1()
t.func2() //*A func2()
p := &A{}
p.func1() //A func1()
p.func2() //*A func2()
}
嵌入类型
- 内部类型:嵌入类型是将已有的类型直接声明在新的结构类型里,被嵌入的类型被称为新的外部类型的内部类型
- 通过嵌入类型,与内部类型相关的标识符将会提升到外部类型上
- 下面的例子好像并不符合方法集规则,但是却行得通:
package main
import (
"fmt"
)
type I interface {
func1()
}
type S struct {
A
}
type A struct{}
func (a *A) func1() {
fmt.Println("*A func1")
}
func func2(i I) {
i.func1()
}
func main() {
var s S
//func2(s) 这样是不行的
func2(&s) //*A func1
s.A.func1() //*A func1
s.func1() //*A func1
}
- 为什么s能直接调用func1(),这个方法的接收器不是指针类型的接收器吗?
因为内部类型相关的标识符将会提升到外部类型上,而且这个s只是个结构体而不是接口,结构体并不用遵守接口值中方法集的规则,结构体更加自由(fun2的调用可以作出对比)
- 如果S也想要有自己的方法func1怎么办?会和S的内部类型的方法func1冲突吗?
package main
import (
"fmt"
)
type I interface {
func1()
}
type S struct {
A
}
func (s *S) func1() {
fmt.Println("*S func1")
}
type A struct{}
func (a *A) func1() {
fmt.Println("*A func1")
}
func func2(i I) {
i.func1()
}
func main() {
var s S
//func2(s) 这样是不行的
func2(&s) //*S func1, 这里内部类型不会被提升,优先使用外部类型的方法
func2(&s.A) //*A func1
s.A.func1() //*A func1
s.func1() //*S func1,这样内部类型就不会被提升了
}
- 当外部类型的方法和内部类型的方法同名时,使用外部类型调用这个方法,内部类型不会被提升,优先调用外部类型的方法
标识符的可见性
- 通常通过大小写开头字母就能控制标识符可见性
- 有时候,可能不希望公开包里某个类型、函数或者方法这样的标识符,一个简单的思路就是小写字母开头来声明这个标识符
- 除了通过大写字母开头来公开标识符,还可使用设计模式中工厂模式
package a
type a int
func New(value int) a {
return a(value)
}
package main
import (
"fmt"
"learn/a"
)
func main() {
t := a.New(100)
fmt.Printf("type: %T\nvalue: %d\n", t, t)
//type: a.a
//value: 100
}
"要让这个行为可行,需要两个理由。第一,公开或者未公开的标识符,不是一个值。第二,短变量声明操作符,有能力捕获引用的类型,并创建一个未公开的类型的变量。永远不能显式创建一个未公开的类型的变量,不过短变量声明操作符可以这么做。"
嵌入类型可见性问题
- 直接看例子
package a
//不公开的a类型
type a struct {
Value int //公开的成员
}
//公开的A类型
type A struct {
a
}
package main
import (
"fmt"
"learn/a"
)
func main() {
var t a.A
t.Value = 10
fmt.Println(t.Value) //这样就访问到了未公开类型a的公开成员了
}