任何语言都有其数据类型和数据结构,Go
语言中的类型比较简单,数据结构也比较精简。
基本类型
同其他编程语言一样,且作为一种强类型的编程语言Golang
也具有很多的变量类型,有了变量名作为标识符,可以实现数据的存储和内存的访问,这是编程语言的基础。常见的基本类型大致相同,有整型,浮点型,字符串,布尔型等,注意字符串在这种语言中已经是基本内置变量类型。
声明变量
Go
语言在声明变量时类型放在后面,这一点有些不同,但是所有变量类型都可以用变量标识符var
来声明。
类型推导声明
在这种语言中有很多的动态语言的特征,比如声明变量的时候自动判断类型。
// This kind of declaration will automatically identify the type.
var name = "Wong"
标识符var
+变量名+类型单独声明
标准的声明方法比较的稳妥,但是很多时候不够方便。只用于声明重要的变量或者全局变量。值得注意的是Golang
变量一旦声明必须使用,这一点非常重要,否则无法编译通过。
var name string = "Wong"
var age int = 19
用var()
批量声明
批量声明能让代码看起来更加的规整,常常同一类的变量用这种方式来声明。
var (
name string = "Wong"
age int = 19
)
采用:=
直接声明加赋值
本人觉得最常用的一种方法,尤其是在多返回值函数的时候可以快速的声明变量并且直接使用。
// Very convenient but cannot be used outside of functions.
flag := true
rate := 2.34
当然,这种也有如下一些方面需要注意。
这种方法必须是声明新的变量
虽然是自动判断类型,但是之后就无法改变类型
取值范围
取值范围在Go
语言中主要是通过类型后面的内存位数来决定的内存空间的大小,也就决定了类型的取值范围。
整型
类型 | 取值范围 |
---|---|
uint8 | 0~255 |
uint16 | 0~65535 |
uint32 | 0~4294967295 |
uint64 | 0~18446744073709551615 |
int8 | -128~127 |
int16 | -32768~32767 |
int32 | -2147483648~2147483647 |
int64 | -9223372036854775808~9223372036854775807 |
浮点型
类型 | 取值范围 |
---|---|
float32 | 32 位浮点数 |
float64 | 64 位浮点数 |
complex64 | 32 位实数和虚数 |
complex128 | 64 位实数和虚数 |
类字符型
byte
字节型的和普通的字符差不多,字符串能够通过切片
变成单个的字节。这种字节的编码方式类似与uint8
。
rune
汉字或者日语字的字符串经过切片
可以编程这种单个字符,如果拆分成其他的可能编码不够会出现乱码。
在
Go
语言中字符是用UTF-8
编码的,好处不言而喻,更多的编码位数可以支持更多中的字符,包括中文。
常量类型
顾名思义,固定不变的量。多用于定义程序运行期间不会改变的那些值。他的声明方法和普通变量除关键字var
和const
,之外并没有太大的差别。
特殊的iota
在Go
语言中有一种特殊的量,他自每个const
关键字开始时为 0,在之后每一行(空行不算)递增一。当有新的关键字时,它就会被编程 0。
const (
_ = iota
kb = 1 << (10 * iota)
mb = 1 << (10 * iota)
gb = 1 << (10 * iota)
tb = 1 << (10 * iota)
)
不赋值则自动相同
使用const
声明常量时可以批量赋值,默认使用上面最后显式赋值。
const (
a = 100
b
c
)
匿名变量
比较神奇的一种变量,他没有空间,像黑洞一般,值被赋给他之后会抛弃。更多的充当一种占位的变量使用。根据具体情况具体分析,匿名变量的特点是一个下画线_
本身就是一个特殊的标识符,被称为空白标识符。
指针变量
指针变量在Golang
中并不复杂,所以只需要记住三点。
&
对变量取地址
*
对指针取对应值指针变量存的是指针的地址
// The use of pointers is basically the same as C++.
var p *int
var a int = 20
p = &a
通常不再频繁的使用指针,只是在函数传参和一些特定情况才使用指针。指针变量指向结构体和类对象的时候也可以直接使用.
成员运算符,而不是和->
区分开来,这是Go
语言中的一个语法糖。
基本结构
在很多语言里,容器是以标准库的方式提供,而在Go
语言中的容器比较少,但是十分的精简,满足了基本的需求。分别是内置的容器类型和container
包中的容器。
数组
数组已经十分熟悉,只是语法上有些不同。在没有赋值的元素位置,会被自动赋上零值,不像某些C++
语言编译器的随机值。
var a [3]int
e := [...]int{1, 3, 5, 7, 9}
- 指定赋值法
还有一种特殊的声明定义方法,是可以直接指定数组中某个元素的值,而不是把一些不重要的统统标记。
var a = [...]int{1: 1, 0: 2}
// Directly specify the value of an element.
- 二维数组
数组的声明基本和普通变量相同,但要在变量类型前加上[size]
限定数组的大小,数组的大小是固定的,不能够改变。
var a [3][4]int
切片
首先要记住:切片基于底层数组,本身只相当于一个框选数组的东西,具体看怎么理解吧。切片是对数组的一种引用
类型,所以更改切片值的同时会影响到底层数组。切片是拥有可变长度的序列,是基于数组的一层封装,我们尤其应该记住他是基于数组的。
地址,切片所在的地址
长度,切片中拥有元素部分的大小
容量,切片的底层数组的容量大小
基本使用
切片的声明类似于数组的,但是他的声明不再需要固定的长度。
myBool := []bool{true,false}
但是切片是对底层数组的引用,所以当只声明了切片但没有初始化时,切片的值等于nil
类似于其他语言里的NULL
或者null
。因为这时的切片不具有底层的数组,所以无法存储数据,在使用时会报错。如果有了底层数组,那么切片修改会修改数组。
var mySlice []int
fmt.Println(nil == mySlice)
- 切片的属性
切片的长度和容量已经说过了,长度很好理解就是切片中包含的元素个数。而容量则是从切片第一个元素开始 ,到底层数组的最后一个内存的大小,至于切片头部之前的底部数组内存则不会算入切片的容量,因为切片是向后扩张的。
- 从数组得到切片
a := [4]int{1, 3, 4, 5}
// cut slices by index
var b = a[:2]
var l int = len(b)
var p int = cap(b)
e := a[1:3]
// It does not care where the slice ends, only the size from the beginning to
// the end of the underlying array is considered.
l = len(e)
c = cap(b)
- 添加元素
切记append()
函数必须要有切片接受返回值。
a := []int{1, 3, 5}
a = append(a, 4)
- 删除元素
删除中间元素
其实利用了append()
函数添加切片的方法覆盖删除。
a = append(a[:2], a[3:]...)
// Represents the slice after expansion, adding one element by one.
删除两端元素
直接进行切片的切片就可以了。
a = a[1:]
a = a[:len(a)-1]
- 拷贝切片
注意拷贝的过程中应该注意 b 是否能够容下 a 的元素,经常使 b 的长度和容量都直接等于 a 的对应值之后在进行拷贝操作,以免拷贝不全。
a := []int{1, 3, 5}
b := []int{2, 4, 6}
// Copy the value directly into another slice.
copy(b, a)
切片扩容的策略
首先判断,如果申请的内存大于原来底层数组容量的 2 倍,则直接至等于申请的容量
否则判断,如果原切片的长度小于 1024,则直接把容量扩大为 2 倍
否则判断,如果原切片的长度大于等于 1024,则循环把容量增加原来的 1/4 直到容量足够
如果最后容量的值溢出,则最终将容量的等于新申请量。
共用底层数组
切片如果通过现有数组产生多个就会出现底层数组共用的场景,这种用法不是很推荐。对于同样使用同一个底层数组的切片,如果进行扩容会出现什么情况呢?
如果追加的元素没有超过底层数组的容量,那么会直接操作共享的底层数组也会影响其他的切片
如果追加的元素超过了容量,那么就会重新申请新底层数组然后把原来的复制到新数组并添加
表
在各种语言中尽管底层实现不同,但是map
总是一种有映射的无序容器。不同于序列容器,并不存在顺序访问,只能通过键值key
来访问对应的value
。在Go
语言中它的底层是由哈希表实现的,这种可以扩容的容器和切片一样需要初始化分配底层的空间才能使用,但不推荐动态扩容。
表的定义
声明类似于切片,记得开辟一块底层空间用来使用,因为他也是引用
类型还是靠底层的数据结构进行的扩容。
myMap = make(map[string]int, 10)
表操作
- 访问元素
访问元素可以起到查找的目的,多亏了多返回值的特点Go
直接查找并且赋值,如果没有找到则会附上nil
并且让标识的变量的值为false
。
myMap["Wong"] = 19
myMap["Sun"] = 18
value, ok := myMap["Li"]
if !ok {
fmt.Printf("Not found")
} else {
fmt.Printf("%d ", value)
}
- 删除元素
使用delete
函数可以删除表中的映射关系。
a = make(map[int]string, 100)
a[1] = "Fuck"
a[3] = "Suck"
delete(a, 3)
- 遍历元素
注意遍历输出元素的顺序与填充顺序无关,不能期望map
在遍历时返回某种期望顺序的结果。如果需要有一定顺序的遍历需要读出来并且对数组或者切片排序。
for key, value := range a {
fmt.Println("%d %d", value, key)
}
new
和make
这种语言提供了我们一种申请动态内存的方法,但是不用我们自己释放非常方便。在一些可以扩容的容器声明时,并不清楚应该有什么样的值,但是大概知道初始化的容器的大小,为了让容器如切片
,map
不等于nil
,可以用make
来申请内存空间。
申请内存
批量申请
make
是用来给slice
,map
,chan
来申请内存的,返回的值是容器本身的类型
// 5 is length, 10 is capacity.
var a []int = make([]int, 5, 10)
经过内存申请后的容器的容量不再为空,就可以进行操作了。如切片在使用时没有底层数组是无法使用的,这样用make
相当于给切片了一个底层数组
底层细节
用make
产生的容器的时候如果创建一个空容器,产生的容器是有底层的,但是这个数组的大小是0
并有指针指向它。
// This creation method does not bind the underlying array.
var arr []int
通过make
创建的空容器,而通过直接类型的产生的是nil
,因为后者在某种意义上更加的空,什么都没有。
错误与异常
在不同的语言中都有接受或者抛出错误的语句用来解决程序在运行时候无法完全通过编码解决的错误,panic
用来主动抛出错误,recover
用来捕获panic
抛出错误。
错误和异常的含义
广义错误
发生非期望的行为。
狭义错误
发生非期望的已知行为,这里的行为我们已经知晓类型。
异常
发生非期待的未知行为,这里的行为我们不知道类型。
解决错误
对于Go
语言,为了保证安全性,所有出现的错误和异常都成为错误。逻辑错误会影响结果,但不会影响程序的正常运行。抛出的运行错误则会让程序结束崩溃。
解决方法
- 异常和捕获
有两种引发情况,第一种是主动调用,第二种是程序产生错误,由运行时检测出并抛出。panic
抛出的错误可以被recover
捕获。而且只有recover
直接存在于defer
后的函数体内才能在函数返回之前捕捉到抛出的错误。
- 错误类型
可以常把错误类型作为函数的最后一个返回值,用来判断函数是否发生了错误。如果error
类型等于nil
则正常,反之则不正常。
func Add(int x) (int, error) {
x = x + 1
err := Error()
return x, err
}
使用场景
panic
当程序遇到无法正确执行的错误,主动调用
panic
函数来结束运行调试的时候用
panic
快速的退出
panic
出现函数停止,但是会把defer
执行完才能向上传递
recover
为了保证程序的容错性,在程序的分支流程上使用
recover
拦截运行时的错误,防止因为小错误直接程序结束崩溃不影响程序大体运行的错误才要捕捉,不要海纳百川
func Error() {
defer func() {
// Accept and display errors, but will not crash the program.
err := recover()
fmt.Println(err)
}()
panic()
return err
}
func main() {
if err := Error(); err != nil {
// code...
}
}