吴云华 / golang类型系统

作者:wuyunhua | 2023-10-08

很多新手对& * [] {}的组合感到困惑,不知道什么时候该用什么,这是因为对 go 类型系统不够了解,本文主要讲解go类型系统的基础知识,帮助大家理解go类型系统。

go有哪些类型?

  • 内置基本类型
    1. 字符串类型string
    2. 布尔类型bool
    3. 数值类型 intX floatX complexX
  • 组合类型
    1. 指针类型
    2. 结构体类型
    3. 函数类型
    4. 容器类型
    5. 通道类型
    6. 接口类型

一些类型的例子

type MyInt int              // int类型
type MyIntPtr *int          // int指针类型
type Book struct {}         // 结构体类型
type Option func(int) int   // 函数类型
type Name []byte            // 切片类型
type Sigal chan<- int       // 通道类型
type Object interface{}     // 接口类型

类型的零值

一个类型的一个实例称为此类型的一个值,一个类型可以有很多不同的值,其中一个为它的零值

  • 切片、映射、函数、通道、指针、接口类型的零值是nil(所以结构体值不能用if obj != nil {}来判断)
  • string类型的零值是""
  • bool类型的零值是false
  • 数值类型的零值是0(所以不能依赖0作为某个标识)
  • 结构体类型的零值很特别,比如结构体S的底层类型是struct{x int; y bool},则它的零值是S{0, false}/S{x: 0, y: false}/S{x: 0}/S{y: false}/S{}

defined type

基本类型可以直接用来申明变量,而组合类型不可以,官方把这种可以直接定义变量的类型叫defined type,而不能直接定义变量的类型叫undefined type

// 一个变量的类型是int
var MyInt int
// 无法说一个变量是容器类型
var MySlice 容器类型?
// 需指定具体是什么类型的容器
var MySlice []int

underlying type

所有类型都有底层类型 underlying type

  • 新声明的类型和源类型共享同一个底层类型
  • 一个内置类型的底层类型是它自己
  • 一个undefined type的底层类型是它自己

法则:在溯源过程中,当遇到一个内置类型或者undefined type时,溯源结束

// 类型MyInt的底层类型是int
type MyInt int
// 类型YourInt的底层类型是int
type YourInt MyInt
// 类型Book的底层类型是struct{}
type Book struct{}
// 类型Name的底层类型是[]MyInt
type Name []MyInt
// 类型Nickname的底层类型是[]MyInt
type Nickname Name

非接口类型的类型转换

  1. 类型和他的别名类型,可以隐式转换
  2. 相同底层类型的两个类型可以显示转换
  3. 相同底层类型的两个类型,如果其中有一个是undefined type,则可以隐式转换
type Student []byte
type Teacher []byte
var stu Student = []byte{'a'}
var tea Teacher = []byte{'b'}
// 这是不可以的
stu = tea
tea = stu

// 这是可以的
stu = Student(tea)
tea = Teacher(stu)

指针类型的转换

也服从上述规则,但额外多出一条,如果指针类型的底层类型不同,但基类型相同,可以显示转换

type MyInt int
type IntPtr *int
type MyIntPtr *MyInt

var a *int = new(int)
var b IntPtr

// 这是可以的
a = b
b = a

var c MyIntPtr

// 这是不可以的
a = c
c = a

var d *MyInt

// 这是可以的
d = (*MyInt)(a)

接口类型的转换

不服从上述规则,如果类型T实现了接口I,则T类型的值都可以隐式转换为类型I

type Book interface{
    Name() string
}

type EnglishBook struct {}
func(b *EngilishBook) Name() string {
    return "english book"
}

var ebook *EnglishBook = &EnglishBook{}
var book Book

// 这是可以的
book = ebook

有一个特殊情况,如果接口的方法集是空,意味着任意类型都实现了这个空接口,则可以发生上述隐式转换

type EnglishBook struct {}
func(b *EngilishBook) Name() string {
    return "english book"
}

var ebook = &EnglishBook{}
var book interface{}

// 这是可以的
book = ebook

var age int = 1
var myage interface{}

// 这是可以的
myage = age

这就是go中著名的万金油类型interface{},如果你不知道变量是什么类型,就用interface{}

如何知道一个接口值到底是什么类型的?类型断言

var a interface{}
a = 1

// 这是可以的
b := a.(int)

// 这会panic
b := a.(int32)

// 如果有第二个参数,第二个参数是bool类型,永不panic
b, ok := a.(int)

type-switch 更强大的断言控制

type Book struct{
    name string
}

var v interface{} = &Book{name:"book"}

// t是类型值,v是接口值
switch t := v.(type) {
    case string:
        return ""
    case int,int32,int64:
        return fmt.Sprint(t)
    case Book:
        return t.name
    default:
        return ""
}

&*[]{}什么时候使用?

  • 类型定义的时候,要么是容器要么是指针,只有* []的组合,不存在&取地址,具体选择什么取决于想要指针还是想要容器
  • 类型实例化的时候,如果是指针类型,就基类型初始化,再取地址&。如果是容器类型,就直接初始化
  • 使用的时候,如果需要取地址的值,用一次*,这里的和类型定义时候的完全无关
var a []*Member     // 结构体指针的切片
var b *[]Member     // 结构体切片的指针
c := []*Member{}    // 结构体指针的切片初始化
d := &[]Member{}    // 结构体切片的指针初始化

var e []*[]*Member  // 结构体切片的指针的切片
var f *[][]*Member  // 结构体切片的切片的指针
g := []*[]*Member{} // 结构体切片的指针的切片初始化
h := &[][]*Member{} // 结构体切片的切片的指针初始化

总结

类型系统很重要,不会类型系统,写代码寸步难行。建议阅读官方的类型系统原文。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.wuyunhua.cn/golang-types