变量
从根本上说,变量相当于是对一块数据存储空间的命名,程序可以通过定义一个变量来申请一块数据存储空间,之后可以通过引用变量名来使用这块存储空间。
变量声明
- 引入
var
关键字
var v1 int
var v2 string
var v3 [10]int // 数组
var v4 []int // 数组切片
var v5 struct {
f int
}
var v6 *int // 指针
var v7 map[string]int // map, key为string类型, value为int类型
var v8 func(a int) int
var (
v1 int
v2 string
)
变量初始化
对于声明变量时需要进行初始化的场景, var关键字可以保留,但不再是必要的元素
var v1 int = 10 // 正确的使用方式1
var v2 = 10 // 正确的使用方式2,编译器可以自动推导出v2的类型
v3 := 10 // 正确的使用方式3,编译器可以自动推导出v3的类型
指定类型已不再是必需的, Go编译器可以从初始化表达式的右值推导出该变量应该声明为哪种类型,这让Go语言看起来有点像动态类型语言,尽管Go语言实际上是不折不扣的强类型语言(静态类型语言)。
变量的赋值
在Go语法中,变量初始化和变量赋值是两个不同的概念。下面为声明一个变量之后的赋值过程:
var v10 int
v10 = 123
Go语言的变量赋值与多数语言一致,但Go语言中提供了C/C++程序员期盼多年的多重赋值功能
i, j = j, i
id, age, name := 1, 20, "tom"
匿名变量
我们在使用传统的强类型语言编程时,经常会出现这种情况,即在调用函数时为了获取一个值,却因为该函数返回多个值而不得不定义一堆没用的变量。在Go中这种情况可以通过结合使
用多重返回和匿名变量来避免这种丑陋的写法,让代码看起来更加优雅。
func GetName() (firstName, lastName, nickName string) {
return "May", "Chan", "Chibi Maruko"
}
// 若只想获得nickName,则函数调用语句可以用如下方式编写
_, _, nickName := GetName()
常量
字面常量
所谓字面常量( literal),是指程序中硬编码的常量
-12
3.14159265358979323846 // 浮点类型的常量
3.2+12i // 复数类型的常量
true // 布尔类型的常量
"foo" // 字符串常量
如果要指定一个值为-12的long类型常量,需要写成-12l,这有点违反人们的直观感觉。 Go语言的字面常量更接近我们自然语言中的常量概念,它是无类型的。只要这个常量在相应类型的值域范围内,就可以作为该类型的常量,比如上面的常量-12,它可以赋值给int、 uint、 int32、int64、 float32、 float64、 complex64、 complex128等类型的变量。
常量的定义
通过const关键字,你可以给字面常量指定一个友好的名字:
const Pi float64 = 3.14159265358979323846
const zero = 0.0 // 无类型浮点常量
const (
size int64 = 1024
eof = -1 // 无类型整型常量
)
const u, v float32 = 0, 3 // u = 0.0, v = 3.0,常量的多重赋值
const a, b, c = 3, 4, "foo"
// a = 3, b = 4, c = "foo", 无类型整型和字符串常量
常量定义的右值也可以是一个在编译期运算的常量表达式,比如
const mask = 1 << 3
由于常量的赋值是一个编译期行为,所以右值不能出现任何需要运行期才能得出结果的表达式,比如试图以如下方式定义常量就会导致编译错误:
const home = os.GetEnv("HOME")
预定义常量
Go语言预定义了这些常量: true
、 false
和iota
。
iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0
,然后在下一个const出现之前,每出现一次iota,其所代表的数字会自动增1。
const ( // iota被重设为0
c0 = iota // c0 == 0
c1 = iota // c1 == 1
c2 = iota // c2 == 2
)
如果两个const的赋值语句的表达式是一样的,那么可以省略后一个赋值表达式。
const ( // iota被重设为0
c0 = iota // c0 == 0
c1 // c1 == 1
c2 // c2 == 2
)
枚举
枚举指一系列相关的常量,比如下面关于一个星期中每天的定义。通过上一节的例子,我们看到可以用在const后跟一对圆括号的方式定义一组常量,这种定义法在Go语言中通常用于定义枚举值。 Go语言并不支持众多其他语言明确支持的enum关键字。
类型
bool
布尔类型不能接受其他类型的赋值,不支持自动或强制的类型转换。只能是true
,false
,或者是表达式
var b bool
b = 1 // 编译错误
b = bool(1) // 编译错误
整形
- int8 、uint8(即byte) 、int16、uint16、int32、uint32、int64、uint64、int、uint、uintptr
- 强制类型转换
value2 = int32(value1)
- 数值运算符:+,-,*、/和%
- 比较运算符:>、 <、 ==、 >=、 <=和!=,
两个不同类型的整型数不能直接比较,比如int8类型的数和int类型的数不能直接比较,但各种类型的整型变量都可以直接与字面常量( literal)进行比较
- 位运算:左移、右移、异或、与、或、取反
浮点类型
- float32、float64
- 浮点数的比较:因为浮点数不是一种精确的表达方式,所以像整型那样直接用==来判断两个浮点数是否相等是不可行的,这可能会导致不稳定的结果
下面是一种推荐的替代方案:
import "math"
// p为用户自定义的比较精度,比如0.00001
func IsEqual(f1, f2, p float64) bool {
return math.Fdim(f1, f2) < p
}
复数类型
复数实际上由两个实数(在计算机中用浮点数表示)构成,一个表示实部( real),一个表示虚部( imag)。
- complex64 // 由2个float32构成的复数类,需要8个字节
- complex128 // 由2个float64构成的复数类,需要16个字节
字符串
在Go语言中,字符串也是一种基本类型。字符串的内容可以用类似于数组下标的方式获取,但与数组不同,字符串的内容不能在初始化后被修改
var str string // 声明一个字符串变量
-
字符串的常用操作
x + y:字符串连接
len(s):字符串长度
s[i]:取字符 -
字符串遍历
// 以字节数组的方式遍历
str := "Hello,世界"
n := len(str)
for i := 0; i < n; i++ {
ch := str[i] // 依据下标取字符串中的字符,类型为byte
fmt.Println(i, ch)
}
// 另一种是以Unicode字符遍历:
str := "Hello,世界"
for i, ch := range str {
fmt.Println(i, ch)//ch的类型为rune
}
- string是不可变的,也就说不能通过
str[0] = 'Z'
的方式来修改字符串,如果需要修改字符串,可以先将string -> []byte 或者 []rune, 在重写转成string
中文字符在UTF-8中占3个字节,英文字符占1个字节
字符类型
在Go语言中支持两个字符类型,一个是byte
(实际上是uint8的别名),代表UTF-8字符串的单个字节的值;另一个是rune
(同uint32),代表单个Unicode字符。
数组类型
数组就是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素( element),一个数组包含的元素个数被称为数组的长度。
常用的数组声明与初始化方法
[32]byte // 长度为32的数组,每个元素为一个字节
[2*N] struct { x, y int32 } // 复杂类型数组
[1000]*float64 // 指针数组
[3][5]int // 二维数组
[2][2][2]float64 // 等同于[2]([2]([2]float64))
var array [3]int = [3]int{1,2,3}
var array = [3]int{1,2,3}
var array = [...]int{1,2,3}
var array = [...]int{1:10, 2:20, 3:30}
array := [...]string{1:"tom", 2:"jack", 3:"mary"}
// 二维数组初始化的方法
var matrix [m][n]int // 遍历初始化
var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}
var 数组名 [大小][大小]类型 = [...][大小]类型{{初值..},{初值..}}
var 数组名 = [大小][大小]类型{{初值..},{初值..}}
var 数组名 = [...][大小]类型{{初值..},{初值..}}
数组的长度是该数组类型的一个内置常量,可以用Go语言的内置函数len()
来获取。下面是一个获取数组arr元素个数的写法:arrLength := len(arr)
- 元素访问与遍历
可以使用数组下标来访问数组中的元素。
for i := 0; i < len(array); i++ {
fmt.Println("Element", i, "of array is", array[i])
}
for i, v := range array {
fmt.Println("Array element[", i, "]=", v)
}
-
值类型
需要特别注意的是,在Go语言中数组是一个值类型( value type)。所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该参数将发生数据复制。因此,在函数体中无法修改传入的数组的内容,因为函数内操作的只是所传入数组的一个副本。 -
数组的注意事项
- 数组的地址可以通过数组名来获取 &array
- 第一个元素的地址,就是数组的首地址
- 数组的各个元素的地址间隔是依据数组的类型决定的
- 数组长度是数组类型的一部分,在传递函数参数时,需要考虑数组的长度
- 数组是值传递类型,如果在其他函数中,去修改原来的数组,可以使用引用传递
func update(arr *[3]int) {
*arr[0] = 10
}
func main() {
var arr [3]int = [3]int{1,2,3}
update(&arr)
}
数组切片
数组切片的数据结构可以抽象为以下3个变量:
- 一个指向原生数组的指针;
- 数组切片中的元素个数;
- 数组切片已分配的存储空间。
- 创建数组切片
创建数组切片的方法主要有两种——基于数组和直接创建,下面我们来简要介绍一下这两种方法。
- 基于数组
// 先定义一个数组
var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 基于数组创建一个数组切片
var mySlice []int = myArray[:5]
Go语言支持用myArray[first:last]这样的方式来基于数组生成一个数组切片
// 基于myArray的所有元素创建数组切片:
mySlice = myArray[:]
// 基于myArray的前5个元素创建数组切片:
mySlice = myArray[:5]
// 基于从第5个元素开始的所有元素创建数组切片:
mySlice = myArray[5:]
- 直接创建
Go语言提供的内置函数make()
可以用于灵活地创建数组切片。
// 创建一个初始元素个数为5的数组切片,元素初始值为0:
mySlice1 := make([]int, 5)
// 创建一个初始元素个数为5的数组切片,元素初始值为0,并预留10个元素的存储空间:
mySlice2 := make([]int, 5, 10)
// 直接创建并初始化包含5个元素的数组切片:
mySlice3 := []int{1, 2, 3, 4, 5}
// 当然,事实上还会有一个匿名数组被创建出来,只是不需要我们来操心而已。
-
元素遍历
操作数组元素的所有方法都适用于数组切片,比如数组切片也可以按下标读写元素,用len()
函数获取元素个数,并支持使用range
关键字来快速遍历所有元素。 -
动态增减元素
数组切片支持Go语言内置的cap()
函数和len()
函数, cap()函数返回的是数组切片分配的空间大小,而len()函数返回的是数组切片中当前所存储的元素个数。
mySlice := make([]int, 5, 10)
fmt.Println("len(mySlice):", len(mySlice))
fmt.Println("cap(mySlice):", cap(mySlice))
如果需要后面继续新增元素,可以使用append()函数。函数append()的第二个参数其实是一个不定参数,我们可以按自己需求添加若干个元素,甚至直接将一个数组切片追加到另一个数组切片的末尾
//从尾端给mySlice加上3个元素
mySlice = append(mySlice, 1, 2, 3)
// 给mySlice后面添加另一个数组切片,需要加入...
mySlice = append(mySlice, mySlice2...)
- 基于数组切片创建数组切片
oldSlice := []int{1, 2, 3, 4, 5}
newSlice := oldSlice[:3] // 基于oldSlice的前3个元素构建新数组切片
有意思的是,选择的oldSlicef元素范围甚至可以超过所包含的元素个数,比如newSlice可以基于oldSlice的前6个元素创建,虽然oldSlice只包含5个元素。只要这个选择的范围不超过oldSlice存储能力(即cap()返回的值),那么这个创建程序就是合法的。 newSlice中超出oldSlice元素的部分都会填上0。
- 内容复制
数组切片支持Go语言的另一个内置函数copy(),用于将内容从一个数组切片复制到另一个数组切片。如果加入的两个数组切片不一样大,就会按其中较小的那个数组切片的元素个数进行复制。
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
- 删除元素
转载
map
- 变量声明
var myMap map[string] PersonInfo
- 创建
使用Go语言内置的函数make()来创建一个新map
myMap = make(map[string] PersonInfo)
myMap = make(map[string] PersonInfo, 100) // cap = 100
- 元素赋值
myMap["1234"] = PersonInfo{"1", "Jack", "Room 101,..."}
- 元素删除
Go语言提供了一个内置函数delete(),用于删除容器内的元素。
delete(myMap, "1234")
- 元素查找
value, ok := myMap["1234"]
if ok { // 找到了
// 处理找到的value
}
判断是否成功找到特定的键,不需要检查取到的值是否为nil,只需查看第二个返回值ok
-
map的key的类型,value的类型
- key:
- bool,数字,string,指针,channel,还可以只包含前面几个类型的接口,接口,数组
- 通常为int、string
- slice、map、还有function不可以,因为这几个没法用 == 来判断
- value:
- 基本和key一样
- key:
-
map切片
var m []map[string]string
m = make([]map[string]string, 2)
-
map排序
- map是无序的
- 若要得到排序的map,可以先对key进行排序,然后根据keyzhi遍历输出即可
-
使用细节
- map是引用数据类型
- map会动态扩容
流程控制
Go语言支持如下的几种流程控制语句:
- 条件语句,对应的关键字为if、 else和else if;
- 选择语句,对应的关键字为switch、 case和select(将在介绍channel的时候细说);
- 循环语句,对应的关键字为for和range;
- 跳转语句,对应的关键字为goto。
条件语句 if-else
- 条件语句不需要使用括号将条件包含起来();
- 无论语句体内有几条语句,花括号{}都是必须存在的;
- 左花括号{必须与if或者else处于同一行;
- 在if之后,条件语句之前,可以添加变量初始化语句,使用
;
间隔;
选择语句 switch
- case/switch后是一个表达式:常量值、变量、一个有返回值的函数等都可以
- case后的各个表达式的值的数据类型,必须和switch的表达式数据类型一致
- case后面可以带多个表达式,使用逗号间隔
- case后面的表达式如果是常量值(字面值),则要求不能重复
- case后面不需要带break
- default语句不是必须的
- switch后也可以不带表达式,类似
if-else
分支来使用,这个时候,switch表达式默认为bool
类型 - switch 后也可以直接声明/定义一个变量,分好结束
- switch穿透-fallthrough,如果在case语句块后增减fallthrough,则会继续执行下一个case
- Type Switch: switch语句还可以被用于 type-switch来判断某个interface变量中实际指向的变量类型
循环语句
Go语言中的循环语句只支持for关键字,而不支持while和do-while结构。
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
// while
for {
if expression {
break;
}
...
}
// do while
for {
...
if expression {
break;
}
}
跳转语句goto
不推荐使用
函数
函数定义
在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。
func 函数名(形参列表) (返回值列表){
...
return 返回值
}
func test0() {
}
func test1(x int) int{
return 1
}
func test2(x int, y int) (int, int){
return 1, 2
}
func test3(x, y int) (res1 int, res2 int){
return 1, 2
}
func test4(x, y int) (res1 int, res2 int){
res1 = 1
res2 = 2
return
}
函数调用
函数调用非常方便,只要事先导入了该函数所在的包,就可以直接按照如下所示的方式调用函数:
import "mymath"// 假设Add被放在一个叫mymath的包中
// ...
c := mymath.Add(1, 2)
小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用
- 包使用的注意事项和细节讨论
- 包名通常是小写字母
- 要使用其他包的函数的时候,使用
import
引入该包,包的路径从GOPATH的src下开始的 - 函数名和变量名的首字母大写才能被访问
- 访问方式:
包名.函数
- 如果包名过长,可以取别名,但是只能用别名访问
- 在同一个包下,不能有相同的函数名,或者全局变量名
- 如果要编译成一个可执行程序文件,就需要将这个包声明为
mian
,
不定参数
- 不定参数类型
不定参数是指函数传入的参数个数为不定数量。为了做到这点,首先需要将函数定义为接受不定参数类型:
func myfunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}
// 支持如下调用
myfunc(2, 3, 4)
myfunc(1, 3, 7, 13)
形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数。 从内部实现机理上来说,类型...type本质上是一个数组切片,也就是[]type,这也是为什么上面的参数args可以用for循环来获得每个传入的参数。它是一个语法糖( syntactic sugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用。
- 不定参数的传递
假设有另一个变参函数叫做myfunc3(args ...int), 下面的例子演示了如何向其传递变参:
func myfunc(args ...int) {
// 按原样传递
myfunc3(args...)
// 传递片段,实际上任意的int slice都可以传进去
myfunc3(args[1:]...)
}
- 任意类型的不定参数
如果你希望传任意类型,可以指定类型为interface{}
。用interface{}传递任意类型数据是Go语言的惯例用法。
多返回值
给返回值命名,就像函数的输入参数一样。返回值被命名之后,它们的值在函数开始的时候被自动初始化为空。在函数中执行不带任何参数的return语句时,会返回对应的返回值变量的值。
Go语言并不需要强制命名返回值,但是命名后的返回值可以让代码更清晰,可读性更强,同时也可以用于文档。
如果调用方调用了一个具有多返回值的方法,但是却不想关心其中的某个返回值,可以简单地用一个下划线“_”来跳过这个返回值
匿名函数和闭包
- 匿名函数
在Go里面,函数可以像普通变量一样被传递或使用,这与C语言的回调函数比较类似。不同的是, Go语言支持随时在代码里定义匿名函数。
f := func(x, y int) int {
return x + y
}
func(ch chan int) {
ch <- ACK
} (reply_chan) // 花括号后直接跟参数列表表示函数调用
- 闭包
-
基本概念
闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)。 -
闭包的价值
闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
函数参数传递方式
- 值传递方式:基本数据类型int系列,float系列,bool,string、数组和结构体struct
- 引用传递方式:指针、slice切片、map、管道chan、interface等
init函数
每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说init会在main函数前被调用
init函数的注意事项和细节
- 如果一个文件同时包含全局变量定义,init函数和main函数,则执行的流程全局变量定义->init函数->main函数
- init函数最主要的作用,就是完成一些初始化的工作
函数使用的注意事项和细节讨论
- 基本数据类型和数组默认都是值传递,即进行值拷贝
- 如果希望函数内的变量能够修改函数外的变量(指的是默认以值传递方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量
- Go函数不支持函数重载
- 在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量,通过该变量可以对函数进行调用
- 函数是一种数据类型,所以可以作为形参
- 自定义数据类型:type 自定义数据类型名 数据类型
内置函数
- len
- new:用来分配内存,主要用来分配值类型,返回指针
num := new(int) // *int
*num = 100
- make:用来分配内存,主要用来分配引用类型:channel、map、slice
错误处理
error接口
defer
- 当go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中,然后继续执行函数下一个语句
- 当函数执行完毕后,再从defer栈中,依次从栈顶取出语句执行(注意栈的顺序)
- 在defer将语句放入到栈时,也会将相关的值拷贝同时入栈
- defer最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源