目录 [−]
本文介绍Go的类型系统,以及类型的比较和语句块。
Go语言包含11种类型,你应该很熟悉了,下面让我们再深入的了解一下每种类型的细节。
- 布尔类型
- 数值类型
- 字符串类型
- 数组类型
- Slice类型
- Struct类型
- 指针类型
- 函数类型
- 接口类型
- Map类型
- Channel类型
bool、数值型类型、rune、字符串都是预定义的类型:
|
|
复合类型array, struct, pointer, function, interface, slice, map 和 channel 由类型字面量构造而成。
每一个类型都有一个底层类型(underlying type),比如下面的代码:
|
|
string
、T1
、T2
的底层类型
- 命名类型(named type) : 由一个确定的类型的名称指定
- 未命名类型(unamed type):由类型字面量指定,类型字面量由既有的类型组成
比如下面的例子中, x的类型就是未命名类型,
|
|
而下面的例子中,y的类型就是命名类型。
|
|
未命名类型的一个重要属性就是用同样类型的未命名类型声明的变量拥有相同的类型,而两个不同的命名类型,即使底层的类型相同,它们的类型也是不同的。
更详细的总结会在下一篇文章中介绍,比如我们再定义两个类型:
|
|
其中 x 和 x2 的类型相同, 而 y 和 y2 的类型却不相同。
命名类型可以定义自己的函数, 而未命名类型确不行。
|
|
比如上例中,[]int
是未命名类型,没办法为它定义方法,所以我们可以像下面的例子一样定义一个命名类型,它的底层类型是[]int
:
|
|
参考
- https://github.com/golang/go/issues/5682
- http://stackoverflow.com/questions/32983546/named-and-unnamed-types
- http://blog.csdn.net/hittata/article/details/51250179
布尔类型
很简单,类型名为bool
,只有两个值: true
和 false
。
参考
数值类型
数值类型包括整数、浮点数和复数。总结如下:
类型 | 说明 | |
---|---|---|
uint8 | 无符号8位整数,(0 to 255)| | |
uint16 | 无符号16位整数, (0 to 65535) | |
uint32 | 无符号32位整数, (0 to 4294967295) | |
uint64 | 无符号64位整数, (0 to 18446744073709551615) | |
int8 | 8位整数, (-128 to 127) | |
int16 | 16位整数, (-32768 to 32767) | |
int32 | 16位整数, (-2147483648 to 2147483647) | |
int64 | 64位整数, (-9223372036854775808 to 9223372036854775807) | |
float32 | IEEE-754 32位浮点数 | |
float64 | IEEE-754 64位浮点数 | |
complex64 | 实部和虚部都是float32 | |
complex128 | 实部和虚部都是float64 | |
byte | uint8的别名 | |
rune | int32的别名 |
在不同的架构上,下面的几种类型可能的位数不同:uint
可能是32位或者64位int
可能是32位或者64位uintptr
是一个足够大的无符号整数,可以代表一个指针的值得
int
和int32
并不是一个相同的类型,尽管在一些环境下它们的位数都是32位。
数组类型
数组代表有限的同一元素类型的对象的序列。对象的数量就是数组的长度, 通过len
方法得到。
数组是一维的,但是你可以构造多维数组:
|
|
数组可以通过下面的方式声明,需要指定它的长度:
|
|
声明和初始化可以合在一起:
|
|
[...]
是根据初始化的元素的数量确定数组的长度。
Slice类型
实际在开发的过程中,我们使用数组的场合比较少,这是因为数组一旦定义,它的长度就不能再发生变化,这和很多其它编程语言的定义是一样的。
更多的情况下我们会使用slice。
Slice描述了数组的一个连续的片段。
|
|
上面的例子中slice
对象代表数组的索引位置为100 ~149的元素。
其实slice数据结构是由SliceHeader
描述的,所以我们可以看到slice是由三个数据描述的:第0个元素的指针
、长度
、容量
。
|
|
在后面的文章中我会介绍通过指针操作SliceHeader。
slice可以从数组中生成,如上面的例子,也可以直接生成:
|
|
在生成的时候可以指定slice的长度和容量,如果没有指定容量(make的第三个参数),则容量和长度相同。
如果不是通过make
创建,而是通过var
的方式声明一个零值的slice,则它的长度和容量都为0。
一旦一个slice创建出来,它的底层元素是由一个数组保存着。slice的容量就是这个数组的长度,可以通过cap
获得。
如果slice的元素的数量超过容量,就需要创建新的数组。这是一个值得注意的地方,如果你初始的时候就可以确定元素的最大数量的情况下,
最好设置slice的容量的值,这样避免数组的重新分配和数据拷贝,提高程序的性能。
index, append, remove 和 copy
slice和数组一样,都是可以通过索引得到某个位置的元素。
|
|
如果超出slice的索引最大值,就会导致panic。
可以往slice增加元素:
|
|
通过append
方法可以往slice增加元素,值得注意的是append的返回值是增加元素后的slice,和原始的slice不同,尽管它们底层的数组可能相同。
如果容量足够,元素就继续增加底层数组中,如果容量不够,则结果slice就会创建新的数组。
|
|
你可以往slice一次增加多个元素:
|
|
注意...
写法,它意味着你可以把一个slice中所有的元素全部增加到另外一个slice的尾部:
|
|
一个值得注意的技巧是可以将一个字符串的byte一次都增加到一个 []byte中:
|
|
要删除slice某个索引的位置,可以通过下面的方法:
|
|
这个例子删除索引2处的元素。
slice的拷贝是通过copy
方法。
|
|
返回结果为 dst和src的长度的最小值。这也就是说,如果dst的长度大,则将src全部元素都复制到dst中。如果src的长度大,则将src的len(dst)个元素复制到dst中。
slice类型再深入
因为slice底层使用数组,而这个数组可能在数组和多个slice中共用,这会带来潜在的问题。
1、对数组中元素的更改会影响slice
下面的例子中我们将数组的第一个值改为100,可以看到slice的第一个值也变了。
|
|
2、如果slice使用不同的数组,则不会有影响
这一条是显而易见的,既然数组都不相同了,当然也没有什么影响了。
但是有时候你不是很容易的发现:
|
|
第三行在增加6到数组的尾部的时候,其实slice的容量已经不够了,所以为返回结果的slice新建了数组。
因此对原始数组的更改不会影响s2,但是对s却有影响,s的第一个值也变了。
3、当两个slice有重叠时,可能会有影响
|
|
这个例子s1和s2都使用相同的数组a,而且它们的容量都是5,不同的是它们的长度分别是1和2。
当往s1增加增加一个元素的时候,它事实上将元素放在的数组的索引为2的位置。这会对原始数组和s2都有影响,
看到检查输出的结果:
|
|
4、 copy的时候有重叠
调用copy
方法的时候也可能会产生副作用,比如下面的例子,copy到s1的操作导致底层的数组改变了,影响了a,s1,s2的值
|
|
5、Slice作为参数传递
Go语言实际上都是按值传递的(指针类型传递的是指针的值)。因此下面的例子中s1并没有被修改,你只能按照returningToo
函数的方式得到修改的slice。
|
|
或者传递slice的指针:
|
|
参考
- https://blog.golang.org/slices
- https://golang.org/pkg/unsafe/
- https://www.reddit.com/r/golang/comments/283vpk/help_with_slices_and_passbyreference/
字符串类型
字符串类型(string)是一个常用的类型,它的值是一组基本按照UTF-8编码的字节序列,可以为空。
在前一篇文章中我已经介绍了字符串类型,所以此处不再重复介绍了,我会介绍一些有趣的性能。
字符串是不可变的,一旦创建,它的值就不能修改了。
内建的len
方法可以得到字符串的长度,每个字节可以根据索引得到,不能像 C 语言一样得到某个字节的地址,&s[i]
这样做是非法的。
你可以把字符串看成一个不可变的slice,比如根据索引得到某个位置的字节,copy操作, append 字符串到[]byte中等:
|
|
字符串和slices of bytes可以很方便的进行互转,因为它们的结构类似,底层都是通过一个数组保存元素的:
|
|
通常情况下,这种转换是对底层数组的复制,所以对转换后的slice的更改不会影响原来的字符串,这也保证了字符串的不可变。
但是,数组的复制是有代价的,内存的分配和数据的拷贝以及垃圾回收都会带来性能等开销,所以在追求性能的场合,比如一些Web框架中,
采用来了一些"花招"实现"零拷贝"。
首先我们看看stirng和slice的数据结构是怎么样的:
|
|
看起来结构类似明知不过slice对string多了一个Cap字段。
所以我们可以根据它们的结构进行转换,不需要拷贝底层的数据Data:
|
|
整数之间、整数和字符串之间、slice和整数之间的转换放在数据转换一文中专门介绍。
参考
- https://golang.org/src/runtime/string.go
- https://golang.org/pkg/reflect/#SliceHeader
- https://github.com/alecthomas/unsafeslice
Struct类型
了解C语言的同学都知道struct(结构体)。
struct是一组命名的元素的序列,每个元素都有名字和类型,这些元素叫做结构体的字段(field)。字段名可能显示地指定,也可能隐式地指定。字段名不能重复。
struct类型有很多有趣的特性。
1、匿名字段
空字段、占位字段自不必说,你应该都已经了解了:
|
|
struct还允许定义匿名字段。 匿名字段指声明了类型的字段,也叫嵌入字段或者嵌入类型。嵌入类型由类型名T或者指向非接口类型的指针*T指定。T本身不能再是指针类型。 类型名作为字段的名字。
下面的例子中的结构体包含四个匿名字段,可以看到包名不会作为字段名的一部分。
|
|
下面的结构体的定义是非法的,因为三个字段重名了。
|
|
如果结构体x有一个嵌入字段E,并且E拥有字段或者方法f,那么你可以直接调用x.f (如果x.f是一个合法的selector的话),
这叫做字段提升(promoted)。
提升的字段就像结构体的正常的字段一样,除了初始化的时候不能像普通的字段设置。
假设有一个struct S和一个类型T, 提升的方法有以下的特性:
- 如果
S
包含一个嵌入字段T
, 那么S
和*S
的方法集包含 receiverT
的方法. 而*S
还包含 receiver*T
的方法. - 如果
S
包含一个嵌入字段*T
, 那么S
和*S
的方法集都包含 receiverT
和*T
的方法.
*S
可以直接访问 S
的方法,而不必求值后再访问。
关于方法集和receiver我们在以后再讲。
字段的声明中还可以包含一个缺省的字符串tag, 用来作为这个字段的属性,通过反射可以得到这个tag的值,在类型比较的时候会进行比较。经常用在结构体的序列化反序列化中,序列化库可以根据这些tag将相应的字段转换成合适的值。
|
|
struct本身也可以是匿名的:
|
|
但是不能把一个匿名struct作为匿名字段,因为Go不知道如何命名此字段:
|
所以必须为匿名struct字段命名,初始化的时候还挺麻烦:
|
|
指针类型
指针类型相对于于C语言,是一个简化版的指针,避免了C语言指针复杂的计算带来的陷阱。
指针类型就是在原有的类型前面加星号 *
。
假设有个操作数x, 类型为T, 那么 &T
则为x的指针,类型为 *T
。
你可以声明指向指针的指针:
|
|
但是不能像C语言一样直接移动指针:
|
|
虽然我们不能直接移动指针,但是我们可以通过曲折的方法操作:
|
|
这个例子中我们成功地将指针移动到第二个索引处,虽然它和 &x[1]的功能是一样的,但却表明我们可以根据偏移量计算指针。
这种方法更多的应用到struct的字段值的读取中,一些序列化的库通过它来读取struct字段的值。
首先我们认识两个对象: uintptr
和unsafe.Pointer
。uintptr
是一个足够大的整数,用来存放指针的位。
unsafe.Pointer
定义如下:
|
|
Pointer代表指向任意类型的指针,它有四个独有的操作:
1) 任意类型的指针可以被转换成一个 Pointer对象.
2) 相反一个Pointer也可以转换成任意类型的指针.
3) 一个uintptr可以转换成一个Pointer.
4) 相反一个Pointer可以转换成uintptr.
通过Pointer我们就可以直接读取内存,使用起来要格外小心。
下面列出了几种转换:
1、*T -> Pointer to T2
|
|
2、Pointer to T2 -> *T
|
|
3、Pointer -> uintptr
|
|
4、uintptr -> Pointer
同上, Pointer(uintptr)转换即可。
内建的new
函数可以为类型T创建零值的对象,它返回的对象类型为*T
。
|
|
参考
函数类型
在Go语言中,函数是第一类的,可以赋值给变量,当做参数传入传出。
参数名和返回值名可以省略,但是如果要省略,必须所有的参数名或者返回值名全省略,部分省略是不行的。
通过...
支持变参,但是变参只能作为最后一个参数。
如果不需要返回值,则不需要定义返回类型。
可以命名函数类型。
以下都是合法的函数类型:
|
|
注意此处介绍的是函数类型,函数的定义可以指定函数名称,也可以定义匿名函数。
接口类型
Go并没有Java那样的完全的面向对象的类型系统, 而是通过接口和duck typing支持面向对象的编程,也就是会呱呱叫的我们都认为它是鸭子。
一个接口代表一组方法的集合。任何实现了这些方法的类型的值都可以赋值给这个接口变量,我们也可以说这些类型实现了这个接口。
特殊的, interface{}
代表一个万能的接口,其它类型都实现了这个接口,有点像Java中的Object类。
接口也可以嵌入,只要保证方法名唯一即可:
|
|
但是接口不能嵌入它本身,或者递归地嵌入本身。
参考
Map类型
Map类型在Go语言中提高到语言规范的级别,在Java只是作为类库中的类实现的。
Map是一组无序的键值对的集合。键的类型相同,值的类型也相同。
Map的变量的定义可以通过下面几种方式,中括号中是键的类型,后边接着是值的类型,键:
|
|
一般通过`make`方法生成,可以设置map的初始容量,但是在增加元素的时候容量会快速增加,这也是一般map集合应对元素扩展的时候的方法。
注意, map的键类型是必须是可以比较的(comparable),比如下面的类型都不能作为键值:
|
|
只有以下类型可以比较,也就是可以应用比较操作符(>、 <、== 等):
- 布尔型
- 整型
- 浮点型
- 复数
- 字符串
- 指针
- Channel
- 接口
- struct
- 数组
内置函数delete
可以根据key删除map对象的entry, 如果map为nil或者键不存在,delete相当于一个空操作:
|
|
而往map中增加元素或者查找元素都是通过索引实现的:
|
|
参考
Channel类型
Channel类型我在另外一篇文章中专门介绍了,本文中就不再赘述: Go Channel 详解
类型的零值
当一个值被创建的时候,无论你是用什么方法,声明或者new方法创建,如果没有显示地初始化,Go就会给它分配一个零值(zero value)。
- 布尔型: false
- 整数: 0
- 浮点数: 0.0
- 字符串: ""
- 指针: nil
- 函数: nil
- 接口: nil
- slice: nil
- channel: nil
- map: nil
值得注意的是字符串,从Java转过来的程序员会以为字符串的零值是nil,其实不是,字符串的零值是空的字符串。
类型的比较
如何判断两个对象的类型是一样的?本节我们来讨论类型一致性(Type identity, 类型完全相同)。
两个类型,要么类型一致,要不类型不同。
1、如果两个命名类型传承自同样的TypeSpec),则它们是类型一致的。这是显然地,因为是自己和自己比较
2、一个命名类型和一个未命名类型肯定是类型不一致
|
|
T0
、T1
、 T2
和 []string
都是不同的。
对于两个未命名类型,如果类型的声明一致,则类型一致,具体如下:
1、两个数组类型一致, 如果它们的元素的类型是类型一致并且数组长度相同
2、两个slice类型一致,如果它们的元素的类型是类型一致
3、两个struct类型一致,如果它们拥有相同的字段序列,并且相应的字段的名字相同而且类型一致,并且tag相同。两个匿名字段被认为有相同的名字。不同包下的Lower-case字段总是不同的
4、两个指针类型相同,如果它们指向的对象的类型是类型一致的
5、两个函数的类型相同,如果它们的参数和返回值的数量一致,并且类型一致,都有变参或者都没有。不要求参数名和返回名一致
6、两个接口类型相同,如果它们的方法集相同,方法名相同,方法的类型一致。不同包下的Lower-case方法名是不同的。方法声明的顺序无关
7、两个map类型相同,如果它们的key和value的类型一致
8、两个channel类型一致,如果它们的值的类型一致,并且有相同的方法(direction)
对于下列类型:
|
|
下列的类型是一致的:
T0
和T0
[]int
和[]int
struct{ a, b *T5 }
和struct{ a, b *T5 }
func(x int, y float64) *[]string
和func(int, float64) (result *[]string)
但是T0
和 T1
类型不同; func(int, float64) *T0
和 func(x int, y float64) *[]string
也不同, 因为 T0
和 []string
不同。
这也容易理解,不同的类型可以拥有不同的方法集。
赋值
一个值x只有在下述情况下才能指派给类型T
(类型为T的一个变量):
- x的类型和
T
类型一致。 相同类型的值当然可以赋值给相同类型的变量。 - x的类型
V
和 类型T
的底层类型一致, 并且至少V
和T
中的一个是未命名类型。
|
|
T
是一个接口, 而 x 实现了接口T
。- x 是双向的channel, 而
T
是一个channel类型,并且 x 的类型V
和T
的元素的类型是类型一致的,并且V
和T
至少有一个是未命名类型。
|
|
上例中ch2可以赋值给CH1
、CH2
类型的变量,因为ch2的类型是未命令类型,但是ch3就不可以赋值ch1,因为它们两个的类型都是命名类型。
- x 是预声明的值
nil
,T
是一个指针、函数、slice、map、channel 或者接口的话,可以赋值,nil
是这些类型的零值。 - x 是一个未标明类型的常量,可以作为类型
T
的值。
|
|
参考
代码块(block)
代码块是用括号"{}"括起来的包含声明和语句的一段代码。可以为空。
事实上当你编写Go代码的时候,就包含了隐式的代码块:
- 全局的语句块包含所有的源码
- package包含本package中的源码
- 每个文件包含一个文件代码块,它包含本文件中的所有源码
if
、for
和switch
语句包含在它们的隐式代码块中switch
、select
中的每一个clause语句都是一个隐式代码块
代码块最重要的特性就是scope,以后讲。
声明一下, 本系列的第一部分基本是按照Go语言的规范编写的,大量参考了Go语言规范的内容
有疑问加站长微信联系(非本文作者)