Go语言基础之变量、常量、内置类型以及设计技巧
上一篇Hello, Go已经接触了一个最简单的Go输出程序,这一篇通过例子来学习一下Go语言中的变量、常量、内置类型以及程序设计中的一些技巧。
变量
Go语言定义变量的方式有很多,使用var
关键字是最基本的一种,与大部分语言不同的是Go把变量类型放在变量名后面
1 | // 定义一个名为"variableName",类型为"type"的变量 |
定义多个变量
1 | // 定义三个类型都为"type"的变量 |
定义变量并初始化值
1 | // 初始化名为"variableName",类型为"type"的变量,值为"value" |
同时初始化多个变量
1 | // 定义三个类型都是"type"的变量,并分别初始化相应的值 |
可以把上面的写法变得更简单一点
1 | // 定义三个变量,分别初始化相应的值,Go会根据相应值的其类型自动初始化它们 |
Go还支持一种简短声明,用:=
符号取代var
,效果和上面的代码一样。只是简短声明有一个限制,只能在函数内部使用;在函数外部无法编译通过,所以一般用var
来定义全局变量
1 | // 和上面代码的效果是一样的 |
_
(下划线)在Go中是特殊的变量名,任何赋予它的值都会被丢弃。比如将35
赋予b
,同时丢弃34
1 | _, b := 34, 35 |
Go是一种强类型语言,任何声明过但未使用的变量在编译时都会报错,比如下面这个例子,抛出错误:声明了变量i
但未使用
1 | package main |
常量
所谓常量,就是程序编译时就确定下来的值,程序运行时无法改变。常量使用const
关键字,可以定义为数值、布尔值或字符串等类型。
语法如下:
1 | // 定义名为"constname",值为"value"的常量 |
下面是一些例子:
1 | const Pi = 3.1415926 |
Go中的常量和一般语言不同的是,可以指定相当多的小数位数(比如200位),若指定了常量类型为float32则自动缩短为32bit,若指定了常量类型为float64则自动缩短为64bit。
内置类型
布尔类型
在Go中,布尔值的类型为bool
,值为true
或false
,默认为false
。看一下示例
1 | var isvalid bool //声明,变量isvalid默认值为false |
数值类型
在Go中,整数类型分为无符号uint
以及有符号int
两种。这两种类型长度相同,但具体长度取决于不同编译器的实现。同样,Go支持定义好位数的类型:rune
,int8
,int16
,int32
,int64
以及byte
,uint8
,uint16
,uint32
,uint64
。其中rune
是int32
的别称,byte
是uint8
的别称。
需要注意的是,不同整数类型之间是不允许互相赋值或操作的,不然在编译时会引起报错。比如下面的代码,就会引起编译报错,报错内容:mismatched types int8 and int32
1 | var a int8 |
虽然int
类型长度为32位,但是和int32
类型也不能互用,这点要注意。下面的代码,同样会引起编译器报错
1 | var a int |
浮点数类型分为float32
以及float64
两种,默认为float64
。
Go还支持复数类型,默认为complex128
,即64位实数+64虚数。由于不常用,这里就不一一展开了。
字符串类型
在Go中,字符串都是采用UTF-8
字符集编码,用一对""
双引号或``反单引号扩起来定义,类型为string
。看一下示例
1 | // 定义了一个值为空的字符串 |
需要注意的是,Go中的字符串是不可变的。比如下面的代码编辑时报错:cannot assign to h[0]
1 | var h string = "hello" |
那如果真要修改应该怎么做呢?
1 | h := "hello" |
和大部分语言相同的是,Go使用+
操作符来连接字符串
1 | h := "hello" |
虽然字符串不支持修改,但是可以进行切片操作
1 | h := "hello" |
Go通过反单引号`可以声明跨行字符串。反单引号`扩起来的字符串为Raw字符串,即不转义,原样输出。
1 | hw := ` hello |
错误类型
在Go中,内置一个error
类型,用来处理错误信息。基础的package
中还有一个专门的errors
包用来处理错误
1 | err := errors.New("emit error") |
数据底层内存分配
可以用一张图来概括基础类型的数据底层都是如何分配内存的
一些技巧
分组声明
在Go中,同时导入多个包、常量或变量,可以采用分组的方式声明
下面的代码1
2
3
4
5
6
7
8
9
10import "fmt"
import "os"
const i = 100
const pi = 3.1415
const prefix = "Go_"
var i int
var pi float32
var prefix string
可以用如下形式分组声明
1 | import( |
iota枚举
在Go中,iota
关键字用来声明枚举,默认值为0,在常量中每增加一行值加1
1 | package main |
数组、动态数组以及字典
数组array
在Go中,数组用array
表示,定义方式如下
1 | // 定义一个名为"arr",类型为"type",长度为"n"的数组 |
和大部分语言相同的是,在Go中操作数组通过[]
方括号来进行读取或赋值,需要注意的是,并不存在关键字array
1 | // 声明了一个长度为10,类型为int的数组arr |
数组还支持简单声明
1 | // 声明了长度为3,类型为int的数组a |
Go同样支持嵌套数组,即多维数组,例如
1 | // 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素 |
数组分配如下图所示
Go语言中,数组作为参数传递时,属于值传递,也就是说传的是数组的副本,不是指针。如果要使用引用传递的话,就需要介绍下面的动态数组slice
类型了。
动态数组slice
在很多场景下,我们在定义数组的时候,并不知道初始长度,此时就需要用上动态数组slice
。类似于Java中的List。
slice
并不是真正意义上的动态数组,其本质是一个引用类型。slice
总是指向一个底层数组,因此它的声明可以和数组一样,只是不需要长度。注意,程序中不存在slice
关键字。
1 | // 声明一个名为"fslice"的数组,只是少了长度,所以就是一个动态数组了 |
slice
动态数组和array
数组在声明时的区别在于,声明数组时,[]
方括号内写明了数组的长度或者使用...
来自动计算数组长度,而声明动态数组时,[]
方括号内没有任何字符。
下面是一些例子
1 | // 声明一个含有10个元素元素类型为byte的数组 |
它们的数据结构如下图所示
slice
在声明的时候有一些简便操作
slice
的默认开始位置是0,ar[:n]
等价于ar[0:n]
slice
的第二个序列默认是数组的长度,ar[n:]
等价于ar[n:len(ar)]
- 如果从一个数组里面直接获取
slice
,可以这样ar[:]
,因为默认第一个序列是0,第二个是数组的长度,即等价于ar[0:len(ar)]
slice
动态数组属于引用类型,如果要改变数组中的值时,推荐使用slice
。下面是slice
作为引用类型的示例
1 | // 声明一个数组 |
从概念上来说,slice
像一个结构体,它包含了三个元素
- 一个指针,指向数组中
slice
指定的开始位置 - 长度,即
slice
的长度 - 最大长度,也就是
slice
开始位置到数组的最后位置的长度
举个例子
1 | Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} |
这段代码的真正存储结构如下图所示
对于slice
有几个有用的内置函数:
len
获取slice
的长度cap
获取slice
的最大容量append
向slice
里面追加一个或者多个元素,然后返回一个和slice
一样类型的slice
copy
函数copy
从源slice
的src
中复制元素到目标dst
,并且返回复制的元素的个数
特别注意:append
函数会改变slice
所引用的数组的内容,从而影响到引用同一数组的其它slice
。 但当slice
中没有剩余空间(即(cap-len) == 0
)时,此时将动态分配新的数组空间。返回的slice
数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的slice
则不受影响。
从Go1.2开始slice支持了三个参数的slice,之前我们一直采用这种方式在slice或者array基础上来获取一个slice
1 | var array [10]int |
这个例子里面slice的容量是8,新版本里面可以指定这个容量
1 | slice = array[2:4:7] |
上面这个的容量就是7-2
,即5。这样这个产生的新的slice就没办法访问最后的三个元素。
如果slice是这样的形式array[:i:j]
,即第一个参数为空,默认值就是0。
字典map
map
类似于Python中字典的概念,它的格式为map[keyType]valueType
。
我们看下面的代码,map
的读取和赋值也和slice
类似,通过key
来操作,只是slice
的index
只能是int
类型,而map
多了很多类型,可以是int
,可以是string
及所有完全定义了==
与!=
操作的类型。
1 | // 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化 |
使用map过程中需要注意的几点:
map
是无序的,每次打印出来的map
都会不一样,它不能通过index
获取,而必须通过key
获取map
的长度是不固定的,也就是和slice
一样,也是一种引用类型- 内置的
len
函数同样适用于map
,返回map
拥有的key
的数量 map
的值可以很方便的修改,通过numbers["one"]=11
可以很容易的把key为one
的字典值改为11
map
和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制(这点在后面章节还会提到,先做个了解)
map
的初始化可以通过key:val
的方式初始化值,同时map
内置有判断是否存在key
的方式,通过delete
删除map
的元素
1 | // 初始化一个字典 |
上面说过了,map
也是一种引用类型,如果两个map
同时指向一个底层,那么一个改变,另一个也相应的改变
1 | m := make(map[string]string) |
make和new操作
写程序中少不了内存的分配。Go使用make和new关键字进行内存分配。需要记住的是,make
用于内建类型(map
、slice
和channel
)的内存分配。new
用于各种类型的内存分配。
new
本质上说跟其它语言中的同名函数功能一样:new(T)
分配了零值填充的T
类型的内存空间,并且返回其地址,即一个*T
类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T
的零值。
new
返回指针。
make(T, args)
与new(T)
有着不同的功能,make
只能创建slice
、map
和channel
(这一章没有介绍channel
,但是不影响),并且返回一个有初始值(非零)的T
类型,而不是*T
。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个slice
,是一个包含指向数据(内部array
)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice
为nil
。对于slice
、map
和channel
来说,make
初始化了内部的数据结构,填充适当的值。
make
返回初始化后的(非零)值。
下图是make和new对应底层的内存分配示例
零值
关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。 此处罗列 部分类型 的 “零值”。
1 | int 0 |
总结
好了,到此为止,我们了解了Go语言中的基础部分,变量、常量、内置基本类型、数组、动态数组、字典以及内存分配操作等。老实说,如果没有看过这一章,直接上手Go的话,数组以及内存分配这一块,可能会影响你对代码的阅读。所以,这些部分是必须要消化的。