本文章主要介绍 Golang 的基本语法。
Quick Guide
一、容器
复杂类型的变量,具有各种形式的存储和处理数据的功能,主要用于编写复杂算法、结构和逻辑。
数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。
声明语法如下:
1 | var 数组变量名 [元素数量]Type |
- 数组变量名:数组声明及使用时的变量名。
- 元素数量:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值。
- Type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。
1 | package main |
默认情况下,数组的每个元素都会被初始化为元素类型对应的零值。
数组之间的对比
可以直接通过较运算符 ==和!=
1 | package main |
多维数组
声明语法如下:
1 | var array_name [size1][size2]...[sizen] array_type |
- array_name 为数组的名字
- array_type 为数组的类型
- size1、size2 等等为数组每一维度的长度。
1 | package main |
切片
切片(slice)是对数组的一个连续片段的引用,就像切糕一样,把数组切出几块,取出其中一块.
语法如下:
1 | slice [开始位置 : 结束位置] |
- slice:表示目标切片对象;
- 开始位置:对应目标切片对象的索引;
- 结束位置:对应目标切片的结束索引。
1 | package main |
切片规则
- 取出的元素数量为:结束位置 - 开始位置;
- 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
- 当缺省开始位置时,表示从连续区域开头到结束位置;
- 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
- 两者同时缺省时,与切片本身等效;
- 两者同时为 0 时,等效于空切片,一般用于切片复位。
1 | package main |
声明切片
声明语法如下:
1 | var name []Type |
切片扩容
1 | package main |
切片删除元素
1 | package main |
切片复制
语法如下:
1 | copy( destSlice, srcSlice []T) int |
多维切片
语法如下:
1 | var sliceName [][]...[]sliceType |
映射
map是一种元素对(pair)的无序集合,pair 对应一个 key(索引)和一个 value(值),所以这个结构也称为关联数组或字典。
声明语法如下:
1 | var mapname map[keytype]valuetype |
- mapname 为 map 的变量名。
- keytype 为键类型。
- valuetype 是键对应的值类型。
- cap 预设的容量。
1 | package main |
map的高级用法
1 | package main |
列表
list是一种非连续的存储容器,由多个节点组成,节点通过变量记录彼此之间的关系,列表有多种实现方法,如单链表、双链表等。
列表的原理可以这样理解:假设 A、B、C 三个人都有电话号码,如果 A 把号码告诉给 B,B 把号码告诉给 C,这个过程就建立了一个单链表结构,如下图所示。
如果在这个基础上,再从 C 开始将自己的号码告诉给自己所知道号码的主人,这样就形成了双链表结构,如下图所示。
那么如果需要获得所有人的号码,只需要从 A 或者 C 开始,要求他们将自己的号码发出来,然后再通知下一个人如此循环,这样就构成了一个列表遍历的过程。
如果 B 换号码了,他需要通知 A 和 C,将自己的号码移除,这个过程就是列表元素的删除操作,如下图所示。
列表的初始化
- 1.通过 container/list 包的 New() 函数初始化 list
1 | 变量名 := list.New() |
- 2.通过 var 关键字声明初始化 list
1 | var 变量名 list.List |
遍历列表
遍历双链表需要配合 Front() 函数获取头元素,遍历时只要元素不为空就可以继续进行,每一次遍历都会调用元素的 Next() 函数
1 | package main |
在列表中插入元素
方 法 | 功 能 |
---|---|
InsertAfter(v interface {}, mark Element) Element | 在 mark 点之后插入元素,mark 点由其他插入函数提供 |
InsertBefore(v interface {}, mark Element) Element | 在 mark 点之前插入元素,mark 点由其他插入函数提供 |
PushBackList(other *List) | 添加 other 列表元素到尾部 |
PushFrontList(other *List) | 添加 other 列表元素到头部 |
1 | package main |
从列表中删除元素
列表插入函数的返回值会提供一个 *list.Element 结构,这个结构记录着列表元素的值以及与其他节点之间的关系等信息,从列表中删除元素时,需要用到这个结构进行快速删除。
直接看例子:
1 | package main |
二、结构体
结构体是一种复合类型,由零个或多个任意类型的值聚合成的实体,每个值都可以称为结构体的成员。
定义如下:
1 | type 类型名 struct { |
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- struct{}:表示结构体类型
- 字段1、字段2……:表示结构体字段名,结构体中的字段名必须唯一。
- 字段1类型、字段2类型……:表示结构体各个字段的类型,可以是结构体。
实例化
1 | package main |
还可以通过以下两种方式实例化:
1 | ins := new(T) |
- T 表示结构体类型
- ins:T 类型被实例化后保存到 ins 变量中,ins 的类型为 *T,属于指针。
初始化
1 | ins := 结构体类型名{ |
1 | import "fmt" |
匿名结构体
1 | ins := struct { |
1 | package main |
三、接口
接口是一组方法的集合,可以理解为抽象的类型。它提供了一种非侵入式的接口。任何类型只要实现了该接口中方法集,那么就属于这个类型。
接口声明的格式:
1 | type 接口类型名 interface{ |
- 接口类型名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer
- 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。(小写屏蔽隐藏机制)
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略
1 | package main |
- 接口的方法 与 实现接口的类型方法格式一致
- 接口中所有方法均被实现
类型断言
如果想判断 一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。
1 | value, ok := x.(T) |
- x 表示一个接口的类型
- T 表示一个具体的类型(也可为接口类型)
1 | package main |
接口排序
其实可以用来排序。内置接口 sort.Interface,它需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式,它就会给你排序
1 | package main |
四、进程
进程:是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
线程:是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。 一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。
协程:协程是一种用户态的轻量级线程,由用户控制协程的调度。线程进程都是同步机制,而协程则是异步,只不过协程能保留上一次调用时的状态,每次切换回来时,就相当于进入上一次调用的状态。
并发: 多线程程序在单核心的 cpu 上运行,取得多个任务,利用时序切换不同任务执行.请看下图:
并行: 多线程程序在多核心的 cpu 上运行.取得多个任务,并同时去执行所取得的这些任务。相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。请看下图:
goroutine
go 并发是 让某个函数独立运行的能力,这个核心就是goroutine。goroutine有点类似协程,一个goroutine就是一个独立的工作单元,运行时通过算法调度这些goroutine来运行,在单个进程里执行成千上万的并发任务。
语法如下:
1 | go 函数名( 参数列表 ) |
- 函数名:要调用的函数名。
- 参数列表:调用函数需要传入的参数。
1 | package main |
使用匿名函数创建goroutine
语法如下:
1 | go func( 参数列表 ){ |
- 参数列表:函数体内的参数变量列表。
- 函数体:匿名函数的代码。
- 调用参数列表:启动 goroutine 时,需要向匿名函数传递的调用参数。
1 | package main |
- 1.一个是主进程执行过快,导致其他协程没有执行;
- 2.多个协程之间用到相同的数据;
等待组
方法名 | 功能 |
---|---|
(wg * WaitGroup) Add(delta int) | 等待组的计数器 +1 |
(wg * WaitGroup) Done() | 等待组的计数器 -1 |
(wg * WaitGroup) Wait() | 当等待组计数器不等于 0 时阻塞直到变 0。 |
使用步骤:
- 1.利用wg.Add(delta)先增加计数器的个数
- 2.再利用wg.Done()减少计数器的个数,最后让计数器等于0
- 3.当等待组计数器等于0,才会执行wg.Wait()后面的代码,不然就一直阻塞
1 | package main |
资源竞争
1 | package main |
预期是4,实际结果 2/3/4之间随机出现. 因为 count 变量没有任何同步保护,所以两个 goroutine 都会对其进行读写,会导致对已经计算好的结果被覆盖,以至于产生错误结果。
锁住共享资源
解决方案1:锁住共享资源
- 原子函数: 以很底层的加锁机制来同步访问整型变量和指针
- atomic.AddInt64 : 安全地加一个整型值的方式
- atomic.LoadInt64 : 安全地读一个整型值的方式
- atomic.StoreInt64 : 安全地写一个整型值的方式
1 | package main |
互斥锁
方案2:使用互斥锁。
- 互斥锁:在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界代码。
- sync.Mutex:暴力锁, 当一个 goroutine 获得了 Mutex 后,其他 goroutine 就只能乖乖等到这个 goroutine 释放该 Mutex。
- Lock() 加锁
- Unlock() 解锁
- sync.RWMutex: 基于sync.Mutex产生的单写多读模型,运行多个 goroutine 读,读的时候不允许写入,写的时候不允许读。
- Lock()和Unlock()用于申请和释放写锁
- RLock()和RUnlock()用于申请和释放读锁
- sync.Mutex:暴力锁, 当一个 goroutine 获得了 Mutex 后,其他 goroutine 就只能乖乖等到这个 goroutine 释放该 Mutex。
1 | package main |
channel
通道(channel)是一种特殊的类型。在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。
通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
声明语法如下:
1 | var 通道变量 chan 通道类型 |
- 通道类型:通道内的数据类型。
- 通道变量:保存通道的变量。
创建语法如下:
1 | 通道实例 := make(chan类型 通道类型, 缓冲大小) |
- chan类型:
- chan<- 只能用来发送
- <-chan 只能用来接收
- chan 用来发送和接收
- 通道类型:和无缓冲通道用法一致,影响通道发送和接收的数据类型。
- 缓冲大小:决定通道最多可以保存的元素数量。
- 无缓冲通道:不设置。进行发送 goroutine 需要被接收后才可以继续发送。接收的goroutine 也同理。
- 缓冲通道:设置大小,缓存大小为设置个数。进行发送goroutine 可以在发送元素达到缓存上限之前,继续发。接收的goroutine 需要通道元素不为零,就可以一直接收。
- 通道实例:被创建出的通道实例。
1 | package main |
- 使用通道收发数据:
1 | 通道变量 <- 值 |
1 | package main |
1 | package main |
超时通道
因为发送元素到通道,如果没有及时被接收,就会造成阻塞.所以我们可以结合select进行超时判断:
语法结构如下:
1 | select { |
1 | package main |
控制性能
使用下面命令设置程序运行占用的cpu核数。(默认使用机器的最大核数)
1 | // 程序运行占用的cpu核数 |
逻辑CPU的数量:
- <1:不修改任何数值。
- =1:单核心执行。
1:多核并发执行。
五、reflect
程序在编译时变量被转换为内存地址,变量名不会被编译器写入到可执行部分,所以在运行程序时程序无法获取自身的信息。但是 反射 可以在程序运行期对程序本身进行访问和修改。
类型 | 作用 |
---|---|
reflect.ValueOf() | 获取输入参数接口中的数据的值,如果为空则返回0 <- 注意是0 |
reflect.TypeOf() | 动态获取输入参数接口中的值的类型,如果为空则返回nil <- 注意是nil |
1 | package main |
- 变量类型是指针与指针指向的元素:需要使用Elem方法。
方法 | 说明 |
---|---|
Field(i int) StructField | 根据索引返回索引对应的结构体字段的信息,当值不是结构体或索引超界时发生宕机 |
NumField() int | 返回结构体成员字段数量,当类型不是结构体或索引超界时发生宕机 |
FieldByName(name string) (StructField, bool) | 根据给定字符串返回字符串对应的结构体字段的信息,没有找到时 bool 返回 false,当类型不是结构体或索引超界时发生宕机 |
FieldByIndex(index []int) StructField | 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息,没有找到时返回零值。当类型不是结构体或索引超界时发生宕机 |
FieldByNameFunc(match func(string) bool) (StructField,bool) | 根据匹配函数匹配需要的字段,当值不是结构体或索引超界时发生宕机 |
1 | package main |
使用 reflect.Type 的 Field() 方法会返回 StructField 结构,这个结构描述结构体的成员信息:
1 | type StructField struct { |
1 | package main |
反射三大定律
- 定律一:反射可以将“接口类型变量”转换为“反射类型对象” :普通变量 -> 接口变量 -> 反射对象:
•
1 | func TypeOf(i interface{}) Type |
1.反射可以将“接口类型变量”转换为“反射类型对象
普通变量 -> 接口变量 -> 反射对象:
- 1.我们调用 reflect.TypeOf(name) 时,name被存储在一个空接口变量中,然后被传递过去;
- 2.接着reflect.TypeOf 或者 ValueOf 对空接口变量转换为 reflect.Type 或 reflect.Value。打个断点看看,如下图所示:
2.反射可以将“反射类型对象”转换为“接口类型变量”
反射对象 -> 接口变量:使用的是Value 的Interface函数,是把实际的值赋值给空接口变量,它的声明如下:
1 | func (v Value) Interface() (i interface{}) |
1 | package main |
3.要修改“反射类型对象”其值必须是“可写的”
- 通过 CanSet 方法判断 reflect.Value 类型变量的“可写性”,看一下下面的例子:
1 | package main |
可写性,其实就是 可以被寻址 。
通过反射修改变量的值
- 1.获取元素的地址,获取其反射值对象
- 2.获取其反射值对象的元素,修改值
1 | package main |
如果变量类型是结构体,需要考虑字段是否可以被导出。(首字符大写,小写会被隐蔽)
1 | package main |
方法
- 取元素、取地址及修改值的属性方法请参考下表:
方法名 | 备 注 |
---|---|
Elem() Value | 取值指向的元素值,类似于语言层*操作。当值类型不是指针或接口时发生宕 机,空指针时返回 nil 的 Value |
Addr() Value | 对可寻址的值返回其地址,类似于语言层&操作。当值不可寻址时发生宕机 |
CanAddr() bool | 表示值是否可寻址 |
CanSet() bool | 返回值能否被修改。要求值可寻址且是导出的字段 |
Set(x Value) | 将值设置为传入的反射值对象的值 |
- 值修改相关方法如下表所示:
Set(x Value) | 将值设置为传入的反射值对象的值 |
---|---|
Setlnt(x int64) | 使用 int64 设置值。当值的类型不是 int、int8、int16、 int32、int64 时会发生宕机 |
SetUint(x uint64) | 使用 uint64 设置值。当值的类型不是 uint、uint8、uint16、uint32、uint64 时会发生宕机 |
SetFloat(x float64) | 使用 float64 设置值。当值的类型不是 float32、float64 时会发生宕机 |
SetBool(x bool) | 使用 bool 设置值。当值的类型不是 bod 时会发生宕机 |
SetBytes(x []byte) | 设置字节数组 []bytes值。当值的类型不是 []byte 时会发生宕机 |
SetString(x string) | 设置字符串值。当值的类型不是 string 时会发生宕机 |
六、文件读写
使用语法如下所示:
1 | func OpenFile(name string, flag int, perm FileMode) (file *File, err error) |
- name :文件的文件名,如果不是在当前路径下需要加上具体路径
- flag:文件的处理参数,表面上是int,实际上是枚举值。,如果要使用多个参数,需要用’|’连接
- O_RDONLY:只读模式打开文件;
- O_WRONLY:只写模式打开文件;
- O_RDWR:读写模式打开文件;
- O_APPEND:写操作时将数据附加到文件尾部(追加);
- O_CREATE:如果不存在将创建一个新文件;
- O_EXCL:和 O_CREATE 配合使用,文件必须不存在,否则返回一个错误;
- O_SYNC:当进行一系列写操作时,每次都要等待上次的 I/O 操作完成再进行;
- O_TRUNC:如果可能,在打开时清空文件。
- perm:文件读/写/执行权限,同unix,有四位。
- 第一位:
- 4 : set uid,设置使文件在执行阶段具有文件所有者的权限.
- 2 : set gid,目录被设置该位后, 任何用户在此目录下创建的文件都具有和该目录所属的组相同的组.
- 1 : sticky bit,防删除位.
- 第二位:文件的所有者拥有的权限,如果同时拥有多种权限,把数字累加
- 4:读
- 2:写
- 1:执行
- 第三位:文件属组成员拥有的权限,如果同时拥有多种权限,把数字累加
- 4:读
- 2:写
- 1:执行
- 第四位:其他用户拥有的权限,如果同时拥有多种权限,把数字累加
- 4:读
- 2:写
- 1:执行
- 第一位:
1 | package main |
1 | package main |
各种格式文件处理
JSON
JSON 文件内容一般是这样子:
1 | {"key1":"value1","key2":"value2","key3":["value3","value4","value5"]} |
我们可以使用内置的 encoding/json 标准库去处理
1 | package main |
XML
对于XML,我们可以使用 encoidng/xml 包
1 | package main |
Gob
Gob(即 Go binary 的缩写),Go语言自己以二进制形式序列化和反序列化程序数据的格式。
1 | package main |
bin
关于其他二进制文件,可以使用 encoding/binary处理。
语法如下:
1 | func Write(w io.Writer, order ByteOrder, data interface{}) error |
- w:可以读出字节流的数据源
- r:可以写入字节流的数据源
- order:指定写入/读取数据的字节序
- binary.LittleEndian:小端模式,按照从低地址到高地址的顺序,存放据的低位字节到高位字节
- binary.BigEndian:大端模式,按照从低地址到高地址的顺序,存放数据的高位字节到低位字节
- data:必须是定长值、定长值的切片、定长值的指针
1 | package main |
zip
1 | package main |
tar
- 压缩的步骤如下:
- 1.创建一个文件 x.tar,然后向 x.tar 写入 tar 头部信息;
- 2.打开要被 tar 的文件,向 x.tar 写入头部信息,然后向 x.tar 写入文件信息;
- 3.当有多个文件需要被 tar 时,重复第二步直到所有文件都被写入到 x.tar 中;
- 4.关闭 x.tar,完成打包。
1 | package main |
- 解压的步骤如下:
- 1.打开文件 x.tar,然后从这个 tar 头部中循环读取存储在这个归档文件内的文件头信息;
- 2.从这个文件头里读取文件名,以这个文件名创建文件,然后向这个文件里写入数据即可;
1 | package main |
各种格式的速度以及大小对比
后缀 | 读取 | 写入 | 大小(KiB) | 读/写LOC | 格式 |
---|---|---|---|---|---|
.gob | 0.3 | 0.2 | 7948 | 21 + 11 =32 | Go二进制 |
.gob.gz | 0.5 | 1.5 | 2589 | ||
json | 4.5 | 2.2 | 16283 | 32+17 = 49 | JSON |
.json.gz | 4.5 | 3.4 | 2678 | ||
.xml | 6.7 | 1.2 | 18917 | 45 + 30 = 75 | XML |
.xml.gz | 6.9 | 2.7 | 2730 | ||
.txt | 1.9 | 1.0 | 12375 | 86 + 53 = 139 | 纯文本(UTF-8) |
.txt.gz | 2.2 | 2.2 | 2514 | ||
.bin | 1.7 | 3.5 | 7250 | 128 + 87 = 215 | 自定义二进制 |
.bin.gz | 1.6 | 2.6 | 2400 |
More info: Golang