Go语言实战读书笔记(1):多维数组,切片,引用类型,接口,嵌入类型,标识符的可见性

前言

  • 本篇笔记基于《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的公开成员了
}

猜你喜欢

转载自www.cnblogs.com/laiyuanjing/p/11273505.html