1、命名类型和未命名类型
命名类型(Named Type)
类型可以通过识符来表示,这种类型称为命名类型。Go语言的基本类型中有20个预声明简单类型都是命名类型,Go语言还有一种命名类型——用户自定义类型。
未命名类型(Unamed Type)
一个类型由预声明类型、关键字和操作符组合而成,这个类型称为未命名类型。未命名类型又称为字面量(Type Literal)(未命名类型和类型字面量二者等价)。
Go语言的基本类型中的复合类型:数组(array)、切片(slice)、字典(map)、通道(channel)、指针(pointer)、函数字面量(function)、结构(struct)和接口(interface)都属于类型字面量,也都是未命名类型。
所以*int、[]int、[2]int、map[k]v都是未命名类型。
注意:前面所说的结果和接口是未命名类型,这里的结构和接口没有使用type格式定义,具体如下示例:
package main
import "fmt"
func main() {
//使用struct字面量声明的是未命名类型
a := struct {
name string
age int
}{"andes", 18}
fmt.Printf("%T\n", a) //struct { name string; age int }
fmt.Printf("%v\n", a) //{andes 18}
b := Person{"tom", 21}
fmt.Printf("%T\n", b) //main.Person
fmt.Printf("%v\n", b)//{tom 21}
}
//使用type声明的是命名类型
type Person struct {
name string
age int
}
Go语言的命名类型和未命名类型如下图:
- 未命名类型和类型字面量是等价的,通常所说的Go语言基本类型中的复合类型就是类型字面量,所以未命名类型、类型字面量和Go语言基本类型中的复合类型三者等价。
- 通常所说的Go语言基本类型中的简单类型就是这20个预声明类型,它们都属于命名类型。
- 预声明类型是命名类型的一种,另一类命名类型是自定义类型。
2、底层类型
所有【类型】都有一个underlying type(底层类型)。底层类型的规则如下:
- 预声明类型(Pre-declared types)和类型字面量(type literals)的底层类型是它们自身。
- 自定义类型type newtype oldtype中newtype的底层类型是逐层递归向下查找的,直到查到的oldtype是预声明类型(Pre-declared types)或类型字面量(type literals)为止。例如:
type T1 string
type T2 T1
type T3 []string
type T4 T3
type T5 []T1
type T6 T5
T1和T2的底层类型都是string,T3和T4的底层类型都是string,T6和T5的底层类型都是[]T1。特别注意这里的T6、T5与T3、T4的底层类型是不一样的,一个是[]T1,另一个是[]string。
底层类型在类型赋值和类型强制转换时会使用.
3、类型相同和类型赋值
类型相同
Go是强类型的语言,编译器在编译时会进行严格的类型校验。两个命名类型是否相同,参考如下:
- 两个命名类型相同的条件是两个类型声明的语句完全相同。
- 命名类型和未命名类型永远不相同。
- 两个未命名类型相同的条件是它们的类型声明字面量的结构相同,并且内部元素的类型相同。
- 通过类型别名语句声明的两个类型相同。
Go1.9引入类型别名语法type T1 = T2,T1的类型完全和T2一样。引入别名主要有如下原因:
- 为了解决新旧包的 迁移兼容问题,比如context包先前并不在标准库里面,后面迁移到了标准库。
- Go的按包进行隔离的机制不太精细,有时我们需要将大包划分为几个小包进行开发,但需要在大包里面暴露全部的类型给使用者。
- 解决新旧类型的迁移问题,新类型先是旧类型的别名,后续的软件都基于新类型编程,在合适的时间将新类型升级为和旧类型不兼容,常用于软件的柔性升级。
类型可直接赋值
不同类型的变量之间一般是不能直接相互赋值的,除非满足一定的条件。下面探讨类型可赋值的条件。
类型为T1的变量a可以赋值给类型为T2的变量b,称为类型T1可以赋值给类型T2,伪代码表述如下:
//a是类型为T1的变量,或者a本身就是i一个字面常量或nil
//如果如下语句可以执行,则称之为类型T1可以赋值给类型T2
var b T2 = a
a可以赋值给变量b必须要满足如下条件中的一个:
- T1和T2的类型相同。
- T1和T2具有相同的底层类型,并且T1和T2里面至少有一个是未命名类型。
- T2是接口类型,T1是具体类型,T1的方法集是T2方法集的超集。
- T1和T2都是通道类型,它们拥有相同的元素类型,并且T1和T2中至少有一个是未命名类型。
- a是预声明标识符nil,T2是pointer、funcition、slice、map、channel、interface类型中的一个。
- a是一个字面常量值,可以用来表示类型T的值
示例如下:
package main
import "fmt"
func main() {
mp := make(map[string]string, 10)
mp["hi"] = "tata"
//mp与ma有相同的底层类型map[string]string,并且是未命名类型,所以mp可以直接赋值给ma
var ma Map = mp
//im与ma虽然具有相同的底层类型map[string]string,但它们中没有一个是未命名类型
//所以不能赋值,如下语句不能通过编译
//var im iMap = ma
ma.Print()
//Map实现了Print(),所以其可以赋值给接口类型变量
var i interface {
Print()
} = ma
i.Print()
s1 := []int{1, 2, 3}
var s2 slice
s2 = s1
s2.Print()
}
type Map map[string]string
func (m Map) Print() {
for _, key := range m {
fmt.Println(key)
}
}
type iMap Map
//只要底层类型是slice、map等支持range的类型字面量,新类型任然可以使用range迭代
func (m iMap) Print() {
for _, key := range m {
fmt.Println(key)
}
}
type slice []int
func (s slice) Print() {
for _, v := range s {
fmt.Println(v)
}
}
4、类型强制转换
由于Go是强类型的语言,如果不满足自动转换的条件,则必须进行强制类型转换。任意两个不相干的类型如果进行强制转换,则必须符合一定的规则。强制类型的语法格式:var a T=(T)(b)
,使用括号将类型和要转换的变量或表达式的值括起来。
非常量类型的变量ⅹ可以强制转化并传递给类型T,需要满足如下任一条件:
- x可以直接赋值给T类型变量。
- x的类型和T具有相同的底层类型。
package main
import "fmt"
func main() {
mp := make(map[string]string, 10)
mp["hi"] = "tata"
//mp与ma具有相同的底层类型map[string]string,并且mp是未命名类型
var ma Map = mp
//im与ma虽然具有相同的底层类型,但是二者中没有一个是字面量类型,不能直接赋值,可以强制进行类型转换
//var im iMap = ma
var im iMap = (iMap)(ma)
ma.Print()
im.Print()
}
type Map map[string]string
func (m Map) Print() {
for _, key := range m {
fmt.Println(key)
}
}
type iMap Map
//只要底层类型是slice、map等支持range的类型字面量,新类型任然可以使用range迭代
func (m iMap) Print() {
for _, key := range m {
fmt.Println(key)
}
}
type slice []int
func (s slice) Print() {
for _, v := range s {
fmt.Println(v)
}
}
- x的类型和T都是未命名的指针类型,并且指针指向的类型具有相同的底层类型。
- x的类型和T都是整型,或者都是浮点型。
- x的类型和T都是复数类型。
- x是整数值或[]byte类型的值,T是string类型。
- x是一个字符串,T是[]byte或[]rune。
字符串和字节切片之间的转换最常见,示例如下:
s := "hello,世界!"
var a []byte
a = []byte(s)
var b string
b = string(a)
var c []rune
c = []rune(s)
fmt.Printf("%T\n", a)//[]uint8 byte是int8的别名
fmt.Printf("%T\n", b)//string
fmt.Printf("%T\n", c)//[]int32 rune是int32的别名
注意:
- 数值类型和string类型之间的相互转换可能造成值部分丢失;其他的转换仅是类型的转换,不会造成值的改变。string和数字之间的转换可使用标准库strconv。
- Go语言没有语言机制支持指针和interger之间的直接转换,可以使用标准库中的unsafe包进行处理。