【Go语言】类型与接口

我会带着你远行 2023-10-03 15:03 149阅读 0赞

类型系统

在这里插入图片描述

自定义类型

自定义类型使用关键字type来定义,格式如下:

  1. type newtype oldtype

代码示例:

  1. type INT int
  2. type Map map[string]string
  3. type Person struct {
  4. name string
  5. age int
  6. }

自定义类型的初始化:

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. name string
  5. age int
  6. }
  7. func main() {
  8. a := Person{
  9. "Jack", 18} // 不推荐的初始化方式
  10. b := Person{
  11. name: "Jack", age: 18} // 推荐
  12. b1 := Person{
  13. name: "Jack",
  14. age: 18, // 因为“}”换行了,这里的“,”是必须的
  15. }
  16. b2 := Person{
  17. age: 18, // 由于指定了成员名,所以初始化时没有顺序要求
  18. name: "Jack",
  19. }
  20. c := new(Person) // 成员都是零值
  21. d := Person{
  22. } // 不推荐
  23. d.name = "Jack"
  24. d.age = 18
  25. fmt.Printf("%v\n", a)
  26. fmt.Printf("%v\n", b)
  27. fmt.Printf("%v\n", b1)
  28. fmt.Printf("%v\n", b2)
  29. fmt.Printf("%v\n", c)
  30. fmt.Printf("%v\n", d)
  31. }

上述初始化方式中推荐的是:

  1. b := Person{
  2. name: "Jack", age: 18} // 推荐

另外一种是通过类构造函数进行初始化,它不是真正的构造函数,而是普通的函数,但是具有构造作用。

结构体中可以存在匿名成员,即只给出了成员的类型,但是没有命名。

一个结构体里面不能同时存在某个类型及其指针的匿名成名。

自定义接口类型:

  1. type Reader interface {
  2. Read(p []byte) (n int, err error)
  3. }

接口类型会在接口中介绍。

类型方法

Go语言的类型方式是一种对类型行为的封装。格式如下:

  1. // 类型方法接收者是值类型
  2. func (t TypeName)MethodName(ParamList) (ReturnList) {
  3. // mothod body
  4. }
  5. // 类型方法接收者是指针
  6. func (t *TypeName)MethodName(ParamList) (ReturnList) {
  7. // mothod body
  8. }

这里的接收者指的是类型实例或者类型指针。

类型方式的特点:

  • 可以为命名类型增加方法,未命名类型不行;
  • 方法的定义必须和类型的定义在同一个包,所以就不能给预声明类型增加方法了;
  • 大写开头的方法可以被外部访问,小写的不行;
  • 新类型不能调用原有类型的方法,但是底层类型支持的运算可以被新类型继承,另外有一个特例就是struct类型。

示例代码:

  1. package main
  2. import "fmt"
  3. type SliceInt []int
  4. func (s SliceInt) Sum() int {
  5. sum := 0
  6. for _, i := range s {
  7. sum += i
  8. }
  9. return sum
  10. }
  11. func (s *SliceInt) Sum1() int {
  12. sum := 0
  13. for _, i := range *s {
  14. sum += i
  15. }
  16. return sum
  17. }
  18. func main() {
  19. var s SliceInt = []int{
  20. 1, 2}
  21. fmt.Printf("%d\n", s.Sum())
  22. fmt.Printf("%d\n", s.Sum1())
  23. }

Sum()的实现中可以直接使用s这个接收者,可以认为它作为方法的第一个参数传递给了Sum()实现函数,因此可以使用,相当于C++类中隐式的this,但是这里没有使用this指针,而是在定义的时候声明在了函数前面,名字也可以自己定义,而且可以是指针或者值。

这里的s.Sums.Sum1被称为方法值,它可以赋值给其它函数变量,然后向普通函数一样使用:

  1. f := s.Sum // s.Sum是一个方法值,s.Sum()是方法值调用
  2. fmt.Printf("%d\n", f()) // f()也是方法值调用

类型方法跟普通函数差别不大,上述的Sum()方法等价于如下的普通函数(注意这里需要传递类型参数了):

  1. func SliceInt_Sum(s SliceInt) int {
  2. sum := 0
  3. for _, i := range s {
  4. sum += i
  5. }
  6. return sum
  7. }
  8. func main() {
  9. var s SliceInt = []int{
  10. 1, 2}
  11. fmt.Printf("%d\n", SliceInt_Sum(s))
  12. }

有一种称为方法表达式的语法,可以将类型方法调用显式地转换为函数调用,下面的代码将类型方法Sum()Sum1()转换成了普通函数调用的形式:

  1. func main() {
  2. var s SliceInt = []int{
  3. 1, 2}
  4. fmt.Printf("%d\n", SliceInt.Sum(s)) // 使用类型本身,而不是它的实例
  5. fmt.Printf("%d\n", (*SliceInt).Sum1(&s)) // 注意接受者其实是指针,所以参数也需要是指针,所以使用了&
  6. f := SliceInt.Sum // 注意这里不是使用类型实例(即方法值),而是类型本身,所以是方法表达式
  7. fmt.Printf("%d\n", f(s))
  8. }

转换之后需要注意接收者作为参数传入了,需要注意传值还是传指针。

还需要注意,虽然这里的Sum()Sum1()作为接收者分别使用了值和指针传递,所以方法内部的实现稍有差异,但是调用的时候都是s.X()

根据接收者的类型,Go语言中有方法集一说:

  • 接收者为值类型的方法集是S(S指的是接收者为值类型的方法的集合,*S指的是接收者为指针类型的方法的集合);
  • 接收者为指针类型的方法集是S和*S;

s.X()的使用需要保证方法集的对应:

  1. type Data struct{
  2. }
  3. func (Data) TestValue() {
  4. } // S方法集
  5. func (*Data) TestPointer() {
  6. } // *S方法集
  7. func main() {
  8. (*Data)(&struct{
  9. }{
  10. }).TestPointer() // 指针类型(*Data)的方法集可以是*S,TestPointer属于*S方法集,有对应
  11. (*Data)(&struct{
  12. }{
  13. }).TestValue() // 指针类型(*Data)的方法集可以是S,TestValue属于S方法集,有对应
  14. (Data)(struct{
  15. }{
  16. }).TestValue() // 值类型(Data)的方法集是S,TestValue属于S方法集,有对应
  17. // (Data)(struct{}{}).TestPointer() // 值类型(Data)的方法集只有S,TestPointer属于*S方法集,没有对应
  18. }

编译器对调用方法会进行自动转换,即使接收者是指针的方法,仍然可以使用值类型变量进行调用,不过这依赖一定的规则:

  1. 通过类型字面值量显式地进行方法值调用和方法表达式调用,在这种情况下编译器不会做自动转换,会进行严格地方法集检测:

    type Data struct{

    1. }

    func (Data) TestValue() {

    1. }

    func (*Data) TestPointer() {

    1. }

    func main() {

    1. // &struct{}{}是类型字面值
    2. (*Data)(&struct{
    3. }{
    4. }).TestPointer() // 方法值调用
    5. (*Data)(&struct{
    6. }{
    7. }).TestValue() // 方法值调用
    8. (Data)(struct{
    9. }{
    10. }).TestValue() // 方法值调用
    11. Data.TestValue(struct{
    12. }{
    13. }) // 方法表达式调用
    14. // (Data)(struct{}{}).TestPointer() // 方法值调用,方法集不匹配
    15. // Data.TestPointer(struct{}{}) // 方法表达式调用,方法集不匹配

    }

  2. 通过类型变量进行方法值调用和方法表达式调用,在这种情况下,使用值调用方式调用时会进行自动转换,使用表达式调用方式调用时编译器不会进行转换,会进行严格地方法集检查:

    type Data struct{

    1. }

    func (Data) TestValue() {

    1. }

    func (*Data) TestPointer() {

    1. }

    func main() {

    1. // 定义类型变量
    2. var a Data = struct{
    3. }{
    4. }
    5. // 方法表达式调用编译器不会自定转换
    6. Data.TestValue(a)
    7. // Data.TestValue(&a) // 错误,因为不会自动转换
    8. (*Data).TestPointer(&a)
    9. // Data.TestPointer(&a) // 错误,因为不会自动转换
    10. // 方法值调用编译器会进行自动转换
    11. a.TestValue()
    12. (&a).TestValue() // 编译器会转换成a.TestValue()
    13. a.TestPointer() // 编译器会转换成(&a).TestPointer()
    14. (&a).TestPointer()

    }

组合和方法集

前面提到使用type定义新类型不会继承原有类型的方法,但是命名结构类型是一个特例:命名结构类型可以嵌套其它的命名结构类型,外层的结构类型可以调用内部成员类型的数据和方法。

  1. package main
  2. import "fmt"
  3. type X struct {
  4. a int
  5. }
  6. type Y struct {
  7. X // 匿名成员
  8. b int
  9. }
  10. type Z struct {
  11. Y
  12. c int
  13. }
  14. func main() {
  15. x := X{
  16. a: 1}
  17. y := Y{
  18. X: x, b: 2}
  19. z := Z{
  20. Y: y, c: 3}
  21. fmt.Printf("%d\n", z.a) // 1
  22. }

这里的z.a相当于z.Y.X.a,而XY的关系,YZ的关系,就是组合,虽然有点像继承,但是组合用来表示这种形式更合适。

需要注意这里Y类型中的XZ类型中的Y都必须是匿名的,否则没有用。yz的实例化中,都直接用了XY来赋初始值,这是访问匿名成员的方式。

另外,也存在同名数据的情况,这个时候就需要全路径了,而不是直接z.a,下面是一个例子:

  1. package main
  2. import "fmt"
  3. type X struct {
  4. a int
  5. }
  6. type Y struct {
  7. X // 匿名成员
  8. a int
  9. }
  10. type Z struct {
  11. Y
  12. a int
  13. }
  14. func main() {
  15. x := X{
  16. a: 1}
  17. y := Y{
  18. X: x, a: 2}
  19. z := Z{
  20. Y: y, a: 3}
  21. fmt.Printf("%d\n", z.a)
  22. fmt.Printf("%d\n", z.Y.a)
  23. fmt.Printf("%d\n", z.Y.X.a)
  24. }

不过在实际的使用当中,最好就不要出现同名的情况。

方法也类似,可以简写,也可以有同名,此时按照从外到内的方式找到同名的方法并调用(当然,只会调用最外层的那个同名方法,而不是每个都调用一遍)。

  1. package main
  2. import "fmt"
  3. type X struct {
  4. a int
  5. }
  6. func (x X) Print() {
  7. fmt.Printf("X: %d\n", x.a)
  8. }
  9. type Y struct {
  10. X // 匿名字段
  11. a int
  12. }
  13. func (y Y) Print() {
  14. fmt.Printf("Y: %d\n", y.a)
  15. }
  16. type Z struct {
  17. Y
  18. a int
  19. }
  20. func (z Z) Print() {
  21. fmt.Printf("Z: %d\n", z.a)
  22. }
  23. func main() {
  24. x := X{
  25. a: 1}
  26. y := Y{
  27. X: x, a: 2}
  28. z := Z{
  29. Y: y, a: 3}
  30. z.Print() // Z: 3
  31. z.Y.Print() // Y: 2
  32. z.Y.X.Print() // X: 1
  33. }

有点类似于子类覆盖父类的方法。

组合也有方法集的规则:

  1. 若类型S包含匿名成员T,则S的方法集包含T的方法集;
  2. 若类型S包含匿名成员*T,则S的方法集包含T和*T的方法集;
  3. 不管类型S中嵌入的匿名成员是T还是*T,*S方法集总是包含T和*T的方法集;

接口

接口是一组方法签名的集合。接口没有具体的实现逻辑,也不能定义数据成员。

一个具体类型实现接口不需要在语法上显式地声明,只要具体类型的方法是接口方法集的超集(即具体类型的方法集包含了接口方法集中的所有方法),就代表该类型实现了接口。

最常用的接口字面量类型是空接口interface{},由于空接口的方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或者传递给空接口,包括非命名类型的实例。

Go语言的接口有两种,接口字面值类型:

  1. interface {
  2. Method1
  3. Method2
  4. // ...
  5. }

命名类型:

  1. type InterfaceName interface {
  2. Method1
  3. Method2
  4. // ...
  5. }

注意这里的Method1Method2是方法声明(= 方法名 + 方法签名)。

接口实现还支持嵌套匿名接口:

  1. type Reader interface {
  2. Read(p []byte) (n int, err error)
  3. }
  4. type Writer interface {
  5. Write(p []byte) (n int, err error)
  6. }
  7. type ReaderWriter interface {
  8. Reader // 匿名接口
  9. Writer // 匿名接口
  10. }

ReaderWriter接口跟下面的是一样的:

  1. type ReaderWriter interface {
  2. Read(p []byte) (n int, err error)
  3. Write(p []byte) (n int, err error)
  4. }

声明新接口类型的特点:

  1. 接口的命名一般以“er”结尾;
  2. 接口定义的内部方法声明不需要使用func关键字;
  3. 在接口定义中,只有方法声明而没有方法实现,方法声明如下:

    MethodName(param-list) (return-list)

接口绑定具体类型的实例的过程称为接口初始化。接口的初始化有两种方式:

  1. 实例赋值接口;
  2. 接口变量a赋值给接口变量b,这要求b的方法集是a的方法集的子集,即b有的方法a也必须要有;

    package main

    import “fmt”

    type Printer interface {

    1. Print()

    }

    type S struct{

    1. }

    func (s S) Print() {

    1. fmt.Printf("Print\n")

    }

    func main() {

    1. var i Printer
    2. fmt.Printf("%T\n", i) // i是nil
    3. // i.Print() // 因为是nil,所以不能直接调用Print()
    4. i = S{
    5. } // i有一个Print方法,而S也实现相同的Print方法,所以可以对接口进行初始化
    6. fmt.Printf("%T\n", i)
    7. i.Print()

    }

上例中i是一个接口,i = S{}就是实例赋值接口来初始化接口,之后才能够调用接口。

需要注意Printer接口中的方法Print()S结构体中的Print()不仅签名需要一样,方法名也需要一样,否则会报错:

  1. func (s S) Print111() {
  2. fmt.Printf("Print\n")
  3. }

这里的方法变成了Print111()就会报错:

  1. # command-line-arguments
  2. .\interface2.go:20:4: cannot use S{
  3. } (type S) as type Printer in assignment:
  4. S does not implement Printer (missing Print method)

同样,签名不一样也不行:

  1. func (s S) Print(i int) {
  2. fmt.Printf("Print\n")
  3. }

这个例子中会报错:

  1. # command-line-arguments
  2. .\interface2.go:20:4: cannot use S{
  3. } (type S) as type Printer in assignment:
  4. S does not implement Printer (wrong type for Print method)
  5. have Print(int)
  6. want Print()

接口绑定的具体实例的类型被称为接口的动态类型;接口被定义时,其类型已经确定下来,这个被称为接口的静态类型

  1. package main
  2. import "fmt"
  3. type Printer interface {
  4. Print()
  5. }
  6. type S struct{
  7. }
  8. func (s S) Print() {
  9. fmt.Printf("SSSS Print\n")
  10. }
  11. type P struct{
  12. }
  13. func (p P) Print() {
  14. fmt.Printf("PPPP Print\n")
  15. }
  16. func main() {
  17. var i Printer
  18. fmt.Printf("%v\n", i) // i是nil
  19. // i.Print() // 因为是nil,所以不能直接调用Print()
  20. i = S{
  21. }
  22. fmt.Printf("%T\n", i)
  23. i.Print()
  24. i = P{
  25. }
  26. fmt.Printf("%T\n", i)
  27. i.Print()
  28. }

这里的SP就是两个动态类型,程序执行的结果:

  1. <nil>
  2. main.S
  3. SSSS Print
  4. main.P
  5. PPPP Print

接口类型是“第一公民“,可以用在任何使用变量的地方,比如:

  1. 作为结构体成员;
  2. 作为函数或方法的形参;
  3. 作为函数或方法的返回值;
  4. 作为其它接口定义的成员。

类型断言

类型断言的语法:

  1. i.(TypeName)

i是接口变量,而不是具体类型变量。TypeName可以是接口类型名,也可以是具体类型名:

  1. 如果是具体类型名,则类型断言用于判断变量i绑定的实例类型是否就是具体类型TypeName
  2. 如果是接口类型名,则类型断言用于判断变量i绑定的实例类型是否同时实现了TypeName接口。

类型断言的使用方式一:

  1. o := i.(TypeName)

如果TypeName是具体类型名,此时如果接口i绑定的实例类型是就是具体类型TypeName,则变量o的类型就是TypeName,变量o的值就是接口绑定的实例值的副本;如果TypeName是接口类型名,如果接口i绑定的实例类型满足接口类型TypeName,则变量o的类型就是接口类型TypeNameo底层绑定的具体类型实例是i绑定的实例的副本;如果上述两种情况都不满足,则程序抛出panic。

类型断言的使用方式二:

  1. if o, ok := i.(TypeName); ok {
  2. // 具体代码
  3. }

如果TypeName是具体类型名,此时如果接口i绑定的实例类型就是具体类型TypeName,则oktrue,变量o的类型就是TypeName,变量o的值就是接口绑定的实例值的副本;如果TypeName是接口类型名,如果接口i绑定的实例类型满足接口类型TypeName,则oktrue,变量o的类型就是接口类型TypeNameo底层绑定的具体类型实例是i绑定的实例的副本;如果上述两种情况都不满足,则okfalse,变量oTypeName类型的零值。

  1. package main
  2. import "fmt"
  3. type Inter interface {
  4. Ping()
  5. Pong()
  6. }
  7. type Anter interface {
  8. Inter
  9. String()
  10. }
  11. type St struct {
  12. name string
  13. }
  14. func (St) Ping() {
  15. fmt.Printf("Ping\n")
  16. }
  17. func (*St) Pong() {
  18. fmt.Printf("Pong\n")
  19. }
  20. func main() {
  21. st := &St{
  22. "Jack"}
  23. var i Inter = st
  24. if o, ok := i.(Inter); ok {
  25. o.Ping() // Ping
  26. o.Pong() // Pong
  27. }
  28. if o, ok := i.(*St); ok {
  29. fmt.Printf("%v\n", o.name) // Jack
  30. }
  31. }

类型查询

类型查询的语法格式:

  1. switch v := i.(type) {
  2. case type1:
  3. // do sth
  4. case type2:
  5. // do sth
  6. default:
  7. // do sth
  8. }

i是接口变量,如果i未初始化,则v的值是nilcase子句后面可以接具体类型名,也可以接接口类型名:

  1. 如果case后面是接口类型名,且接口变量i绑定的实例类型实现了该接口类型的方法,则匹配成功,v的类型是接口类型,v底层绑定的实例是i绑定具体类型实例的副本;
  2. 如果case后面是一个具体类型名,且接口变量i绑定的实例类型和该具体类型相同,则匹配成功,此时v就是该具体类型变量,v的值是i绑定的实例值的副本;
  3. 如果case后面跟着多个类型,使用逗号分隔,接口变量i绑定的实例类型只要和其中一个类型匹配,则直接使用i赋值给v,相当于v := i
  4. 如果所有的case子句都不满足,则执行default子句,此时执行的仍然是v := i

注意fallthrough语句不能在这里使用。

空接口

空接口表示为interface{}

如果一个函数需要接收任意类型的参数,则参数类型可以使用空接口类型。

空接口是反射实现的基础。

空接口不是真的为空,它不是nil

发表评论

表情:
评论列表 (有 0 条评论,149人围观)

还没有评论,来说两句吧...

相关阅读

    相关 GO语言笔记--接口

    1.接口interface 1)接口是一个或多个方法签名的集合 2)只要某个类型拥有该接口的所有方法签名,即算实现该接口无需显示声明实现了哪个接口,这称为Structu

    相关 GO语言笔记--接口

    1.接口interface 1)接口是一个或多个方法签名的集合 2)只要某个类型拥有该接口的所有方法签名,即算实现该接口无需显示声明实现了哪个接口,这称为Structu