GO语言学习笔记
1.GO语言基础
1.1常量声明
const limit = 512
const top uint16 = 1421
const Pi float64 = 3.1415926
const x,y int = 1,3 //多重赋值
const (
Cyan = 0
Black = 1
White = 2
)
const (
a = iota //a == 0
b = iota //b ==1
c = iota //c == 2
)
const d = iota //d==0,因为const的出现,iota被重置为0
1.2变量声明
var a int
var b string
var c float64
var d [5] int //数组
var e [] int //数组切片
var f * int //正确
var v1 int = 5 //正确
var v2 = 5 //正确,编译器自动推导出V2类型
v3 := 5 //正确,编译器自动推导出V3的类型
//多重赋值
i := 2
j := 3
i, j = j, i //交换i和j的值,此时i == 3, j == 2
1.3 数据类型
1.3.1整型
类型 | 说明 |
---|---|
byte | 等同于 uint8 |
int | 依赖于不同平台下的实现,可以是 int32 或者 int64 |
int8 | [-128, 127] |
int16 | [-32768, 32767] |
int32 | [-2147483648, 2147483647] |
int64 | [-9223372036854775808, 9223372036854775807] |
rune | 等同于 int32 |
uint | 依赖于不同平台下的实现,可以是 uint32 或者 uint64 |
uint8 | [0, 255] |
uint16 | [0, 65535] |
uint32 | [0, 4294967295] |
uint64 | [0, 18446744073709551615] |
uintptr | 一个可以恰好容纳指针值的无符号整型(对 32 位平台是 uint32, 对 64 位平台是 uint64) |
例子:
package main
import (
"fmt"
"unsafe"
)
func main() {
a := 12
fmt.Println("length of a: ", unsafe.Sizeof(a))
var b int = 12
fmt.Println("length of b(int): ", unsafe.Sizeof(b))
var c int8 = 12
fmt.Println("length of c(int8): ", unsafe.Sizeof(c))
var d int16 = 12
fmt.Println("length of d(int16): ", unsafe.Sizeof(d))
var e int32 = 12
fmt.Println("length of e(int32): ", unsafe.Sizeof(e))
var f int64 = 12
fmt.Println("length of f(int64): ", unsafe.Sizeof(f))
}
Output:
length of a: 8
length of b(int): 8
length of c(int8): 1
length of d(int16): 2
length of e(int32): 4
length of f(int64): 8
1.3.2浮点型
类型 | 说明 |
---|---|
float32 | ±3.402 823 466 385 288 598 117 041 834 845 169 254 40x1038 计算精度大概是小数点后 7 个十进制数 |
float64 | ±1.797 693 134 862 315 708 145 274 237 317 043 567 981x1038 计算精度大概是小数点后 15 个十进制数 |
complex32 | 复数,实部和虚部都是 float32 |
complex64 | 复数,实部和虚部都是 float64 |
1.3.3布尔型
var a bool
a = true
b := (2 == 3) //b也会被推导为bool类型
//错误示范
var b bool
b = 1 //编译错误
b = bool(1) //编译错误
1.3.4字符串
Go 语言中的字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。UTF-8 是被广泛使用的编码格式,是文本文件的标准编码,其它包括 XML 和 JSON 在内,也都使用该编码。由于该编码对占用字节长度的不定性,Go 中的字符串也可能根据需要占用 1 至 4 个字节,这与其它语言如 C++、Java 或者 Python 不同。Go 这样做的好处是不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。
Go 语言中字符串的可以使用双引号( " )或者反引号( ` )来创建。双引号用来创建可解析的字符串字面量,所谓可解析的是指字符串中的一些符号可以被格式化为其他内容,如\n在在输出时候会被格式化成换行符, 如果需要按照原始字符输出必须进行转义。而反引号创建的字符串原始是什么样,那输出还是什么,不需要进行任何转义。以下是几个例子:
语法 | 描述 |
---|---|
s += t | 将字符串 t 追加到 s 末尾 |
s + t | 将字符串 s 和 t 级联 |
s[n] | 从字符串 s 中索引位置为 n 处的原始字节 |
s[n:m] | 从位置 n 到位置 m-1 处取得的字符(字节)串 |
s[n:] | 从位置 n 到位置 len(s)-1 处取得的字符(字节)串 |
s[:m] | 从位置 0 到位置 m-1 处取得的字符(字节)串 |
len(s) | 字符串 s 中的字节数 |
len([]rune(s)) | 字符串 s 中字符的个数,可以使用更快的方法 utf8.RuneCountInString() |
[ ]rune(s) | 将字符串 s 转换为一个 unicode 值组成的串 |
string(chars) | chars 类型是[]rune 或者[]int32, 将之转换为字符串 |
[ ]byte(s) | 无副本的将字符串 s 转换为一个原始的字节的切片数组,不保证转换的字节是合法的 UTF-8 编码字节 |
1.3.4.1格式化字符串
Go 语言标准库中的fmt包提供了打印函数将数据以字符串形式输出到控制台,文件,其他满足io.Writer接口的值以及其他字符串。目前为止我们使用了fmt.Printf和fmt.Println,对于前者的使用,就像 C 语言中的 printf 函数一样,我们可以提供一些格式化指令,让 Go 语言对输出的字符串进行格式化。同样的我们可以使用一些格式化修饰符,改变格式化指令的输出结果, 如左对齐等。常用的格式化指令如下:
格式化指令 | 含义 |
---|---|
%% | %字面量 |
%b | 一个二进制整数,将一个整数格式化为二进制的表达方式 |
%c | 一个 Unicode 的字符 |
%d | 十进制数值 |
%o | 八进制数值 |
%x | 小写的十六进制数值 |
%X | 大写的十六进制数值 |
%U | 一个 Unicode 表示法表示的整形码值,默认是 4 个数字字符 |
%s | 输出以原生的 UTF-8 字节表示的字符,如果 console 不支持 UTF-8 编码,则会输出乱码 |
%t | 以 true 或者 false 的方式输出布尔值 |
%v | 使用默认格式输出值,或者使用类型的 String()方法输出的自定义值,如果该方法存在的话 |
%T | 输出值的类型 |
常用的格式化指令修饰符如下:
语法 | 描述 |
---|---|
空白 | 如果输出的数字为负,则在其前面加上一个减号"-"。如果输出的是整数,则在前面加一个空格。使用 %x 或者 %X 格式化指令输出时,会在结果之间添加一个空格。例如 fmt.Printf("% X", “实”)输出 E5 AE 9E |
%#o | 输出以 0 开始的八进制数据 |
%#x | 输出以 0x 开始的十六进制数据 |
+ | 让格式化指令在数值前面输出+号或者-号,为字符串输出 ASCII 字符(非 ASCII 字符会被转义),为结构体输出其字段名 |
- | 让格式化指令将值向左对齐(默认值为像右对齐) |
0 | 让格式指令以数字 0 而非空白进行填充 |
1.3.5数组
Go 语言的数组是一个定长的序列,其中的元素类型相同。多维数组可以简单地使用自身为数组的元素来创建。数组的元素使用操作符号[ ]来索引,索引从 0 开始,到 len(array)-1 结束。数组使用以下语法创建:
[length]Type
[N]Type{value1, value2, …, valueN}
[…]Type{value1, value2, …, valueN}
如果使用了…(省略符)操作符,Go 语言会为我们自动计算数组的长度。在任何情况下,一个数组的长度都是固定的并且不可修改。数组的长度可以使用len()函数获得。由于数组的长度是固定的,因此数组的长度和容量都是一样的,因此对于数组而言cap()和len()函数返回值都是一样的。数组也可以使用和切片一样的语法进行切片,只是其结果为一个切片,而非数组。同样的,数组也可以使用range进行索引访问。
1.3.6切片
一般而言,Go 语言的切片比数组更加灵活,强大而且方便。数组是按值传递的(即是传递的副本),而切片是引用类型,传递切片的成本非常小,而且是不定长的。而且数组是定长的,而切片可以调整长度。创建切片的语法如下:
make([ ]Type, length, capacity)
make([ ]Type, length)
[ ]Type{}
[ ]Type{value1, value2, …, valueN}
内置函数make()用于创建切片、映射和通道。当用于创建一个切片时,它会创建一个隐藏的初始化为零值的数组,然后返回一个引用该隐藏数组的切片。该隐藏的数组与 Go 语言中的所有数组一样,都是固定长度,如果使用第一种语法创建,那么其长度为切片的容量capacity;如果是第二种语法,那么其长度记为切片的长度length。一个切片的容量即为隐藏数组的长度,而其长度则为不超过该容量的任意值。另外可以通过内置的函数append()来增加切片的容量。切片可以支持以下操作:
package main
import (
"fmt"
)
func main() {
a := [...]int{1, 2, 3, 4, 5, 6, 7}
fmt.Printf("len and cap of array %v is: %d and %d\n", a, len(a), cap(a))
fmt.Printf("item in array: %v is:", a)
for _, value := range a {
fmt.Printf("% d", value)
}
fmt.Println()
s1 := a[3:6]
fmt.Printf("len and cap of slice: %v is: %d and %d\n", s1, len(s1), cap(s1))
fmt.Printf("item in slice: %v is:", s1)
for _, value := range s1 {
fmt.Printf("% d", value)
}
fmt.Println()
s1[0] = 456
fmt.Printf("item in array changed after changing slice: %v is:", s1)
for _, value := range a {
fmt.Printf("% d", value)
}
fmt.Println()
s2 := make([]int, 10, 20)
s2[4] = 5
fmt.Printf("len and cap of slice: %v is: %d and %d\n", s2, len(s2), cap(s2))
fmt.Printf("item in slice %v is:", s2)
for _, value := range s2 {
fmt.Printf("% d", value)
}
fmt.Println()
}
以上代码中,我们首先创建了一个数组,数组的长度是由 Go 语言自动计算出的(省略号语法),然后通过切片操作从数组a中创建了切片s1,接着我们修改了该切片的第一个位置的数值,然后发现数组a中的值也发生了变化。最后我们通过make()函数创建了一个切片,该切片的长度和容量分别为 10 和 20,还可以发现 Go 语言将未初始化的项自动赋予零值。运行代码输出如下:
len and cap of array [1 2 3 4 5 6 7] is: 7 and 7
item in array: [1 2 3 4 5 6 7] is: 1 2 3 4 5 6 7
len and cap of slice: [4 5 6] is: 3 and 4
item in slice: [4 5 6] is: 4 5 6
item in array changed after changing slice: [456 5 6] is: 1 2 3 456 5 6 7
len and cap of slice: [0 0 0 0 5 0 0 0 0 0] is: 10 and 20
item in slice [0 0 0 0 5 0 0 0 0 0] is: 0 0 0 0 5 0 0 0 0 0
1.4包
这一节我们介绍 Go 语言基础的最后一个知识点——包。前面我们了解过 Go 语言组织代码的方式是包,包是各种类型和函数的集合。在包中,如果标示符(类型名称,函数名称,方法名称)的首字母是大写,那这些标示符是可以被导出的,也就是说可以在包以外直接使用。前面我们也提到了$GOPATH环境变量(指向一个或多个目录),以及其子目录src目录的,当我们使用import关键字导入包的时候,Go 语言会在$GOPATH,GOROOT目录中搜索包。
我们创建的自定义的包最好放在$GOPATH的src目录下,如果这个包只属于某个应用程序,可以直接放在应用程序源代码的子目录下,但如果我们希望这个包可以被其他的应用程序共享,那就应该放在$GOPATH的src目录下,每个包单独放在一个目录里,如果两个不同的包放在同一目录下,会出现名字冲突的编译错误。作为惯例,包的源代码应该放在一个同名的文件夹下面。同一个包可以有任意多的源文件,文件名的名字也没有任何规定
2.并发编程
你很可能从某种途径听说过 Go 语言。它越来越受欢迎,并且有充分的理由可以证明。 Go 快速、简单,有强大的社区支持。学习这门语言最令人兴奋的一点是它的并发模型。 Go 的并发原语使创建多线程并发程序变得简单而有趣。
2.1 并发与并行
并发指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,通过 cpu 时间片轮转使多个进程快速交替的执行。而并行的关键是你有同时处理多个任务的能力。并发和并行都可以是很多个线程,就看这些线程能不能同时被(多个)cpu 执行,如果可以就说明是并行,而并发是多个线程被(一个)cpu 轮流切换着执行。一个经典且通俗易懂的例子这样解释并发与并行的区别:并发是两个队列,使用一台咖啡机;并行是两个队列,使用两台咖啡机。如果串行,一个队列使用一台咖啡机,那么哪怕前面那个人有事出去了半天,后面的人也只能等着他回来才能去接咖啡,这效率无疑是最低的。
2.2 协程
协程也叫轻量级线程。与传统的进程和线程相比,协程最大的优点就在于其足够“轻”,操作系统可以轻松创建上百万个协程而不会导致系统资源枯竭,而线程和进程通常最多不过近万个。而多数语言在语法层面上是不支持协程的,一般都是通过库的方式进行支持,但库的支持方式和功能不够完善,经常会引发阻塞等一系列问题,而 Go 语言在语法层面上支持协程,也叫goroutine。这让协程变得非常简单,让轻量级线程的切换管理不再依赖于系统的进程和线程,也不依赖 CPU 的数量。
2.3 goroutine
goroutine是 Go 语言并行设计的核心。goroutine是一种比线程更轻量的实现,十几个goroutine可能在底层就是几个线程。 不同的是,Golang 在 runtime、系统调用等多方面对goroutine调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前goroutine的 CPU (P) 转让出去,让其他goroutine能被调度并执行,也就是 Golang 从语言层面支持了协程。要使用goroutine只需要简单的在需要执行的函数前添加go关键字即可。当执行goroutine时候,Go 语言立即返回,接着执行剩余的代码,goroutine不阻塞主线程。下面我们通过一小段代码来讲解go的使用:
//首先我们先实现一个 Add()函数
func Add(a, b int) {
c := a + b
fmt.Println(c)
}
go Add(1, 2) //使用go关键字让函数并发执行
Go 的并发执行就是这么简单,当在一个函数前加上go关键字,该函数就会在一个新的 goroutine 中并发执行,当该函数执行完毕时,这个新的 goroutine 也就结束了。不过需要注意的是,如果该函数具有返回值,那么返回值会被丢弃。所以什么时候用go还需要酌情考虑。
package main
import "fmt"
func Add(a, b int) {
c := a + b
fmt.Println(c)
}
func main() {
for i := 0; i < 10; i++ {
go Add(i, i)
}
}
执行 goroutine.go 文件会发现屏幕上什么都没有,但程序并不会报错,这是什么原因呢?原来当主程序执行到 for 循环时启动了 10 个goroutine,然后主程序就退出了,而启动的 10 个goroutine还没来得及执行 Add()函数,所以程序不会有任何输出。也就是说主goroutine并不会等待其他goroutine执行结束。那么如何解决这个问题呢?Go 语言提供的信道(channel)就是专门解决并发通信问题的.
2.4 Channel
channel是goroutine之间互相通讯的东西。类似我们 Unix 上的管道(可以在进程间传递消息),用来goroutine之间发消息和接收消息。其实,就是在做goroutine之间的内存共享。channel是类型相关的,也就是说一个channel只能传递一种类型的值,这个类型需要在channel声明时指定。
2.4.1 声明与初始化
channel的一般声明形式:var chanName chan ElementType 与普通变量的声明不同的是在类型前面加了channel关键字,ElementType则指定了这个channel所能传递的元素类型。示例:
var a chan int //声明一个传递元素类型为int的channel
var b chan float64
var c chan string
初始化一个channel也非常简单,直接使用 Go 语言内置的make()函数,示例:
a := make(chan int) //初始化一个int型的名为a的channel
b := make(chan float64)
c := make(chan string)
channel最频繁的操作就是写入和读取,这两个操作也非常简单,示例:
a := make(chan int)
a <- 1 //将数据写入channel
z := <-a //从channel中读取数据
2.4.2 select
select用于处理异步 IO 问题,它的语法与switch非常类似。由select开始一个新的选择块,每个选择条件由case语句来描述,并且每个case语句里必须是一个channel操作。它既可以用于channel的数据接收,也可以用于channel的数据发送。如果select的多个分支都满足条件,则会随机的选取其中一个满足条件的分支。
2.4.3 超时机制
通过前面的内容我们了解到,channel 的读写操作非常简单,只需要通过<-操作符即可实现,但是channel的使用不当却会带来大麻烦.我们先来看之前的一段代码:
a := make(chan int)
a <- 1
z := <-a
观察上面三行代码,第 2 行往channel内写入了数据,第 3 行从channel中读取了数据,如果程序运行正常当然不会出什么问题,可如果第二行数据写入失败,或者channel中没有数据,那么第 3 行代码会因为永远无法从a中读取到数据而一直处于阻塞状态。相反的,如果channel中的数据一直没有被读取,那么写入操作也会一直处于阻塞状态。如果不正确处理这个情况,很可能会导致整个goroutine锁死,这就是超时问题。Go 语言没有针对超时提供专门的处理机制,但是我们却可以利用select来巧妙地实现超时处理机制,下面看一个示例:
t := make(chan bool)
go func {
time.Sleep(1e9) //等待1秒
t <- true
}
select {
case <-ch: //从ch中读取数据
case <-t: //如果1秒后没有从ch中读取到数据,那么从t中读取,并进行下一步操作
}
这样的方法就可以让程序在等待 1 秒后继续执行,而不会因为 ch 读取等待而导致程序停滞,从而巧妙地实现了超时处理机制,这种方法不仅简单,在实际项目开发中也是非常实用的。
2.4.4 channel 的关闭
channel的关闭非常简单,使用 Go 语言内置的close()函数即可关闭channel,示例 :
ch := make(chan int)
close(ch)
关闭了channel后如何查看channel是否关闭成功了呢?很简单,我们可以在读取channel时采用多重返回值的方式,示例:
x, ok := <-ch
通过查看第二个返回值的 bool 值即可判断channel是否关闭,若为false则表示channel被关闭,反之则没有关闭。