Go语言开发笔记
一、Go开发环境
1. GOROOT
就是Go的安装路径。
2. GOPATH
GOPATH 是作为编译后二进制的存放目的地和 import 包时的搜索路径 (其实也是你的工作目录,你可以在 src 下创建你自己的 go 源文件,然后开始工作)。
GOPATH 之下主要包含三个目录:
-
bin
:目录主要存放可执行文件; -
pkg
:目录存放编译好的库文件; -
src
:目录下主要存放 Go 的源文件。
3. 程序的运行
$ go run xxx.go // go run 命令, 将 .go源文件进行编译、链接,然后运行生产可执行文件
$ go build xxx.go // go build 命令,编译生成一个可执行程序
二、Go语言基础
1. 常量
const limit = 512
const top uint16 = 1421
const Pi float64 = 3.1415926
const x, y, z int = 1, 3, 5 //多重赋值
iota
是一个可以被编译器修改的常量,在 const
关键字出现时被重置为 0,在下一个 const
出现之前,每出现一次 iota
,其所代表的数字自动加 1。
const (
a = iota // a == 0
b = iota // b == 1
c = iota // c == 2
)
const d = iota // d == 0, 因为const的出现,iota被重置为0。
2. 变量
var a int
var b string
var c float64
var d [5] int // 数组
var e [] int // 数组切片
var f int = 5
var g = 55 // 自动类型推导(省略了int的显式声明)
h := 666 // 省略 var,省略 int
交换两个变量:
x := 2
y := 3
x, y = y, x // Go支持多重赋值
3. 数据类型
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) |
查看某数据类型的字节长度:
var x int64 = 12
fmt.Println("length of int64 :", unsafe.Sizeof(x)) // length of int64 : 8
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 |
3.3 布尔型
go语言提供了布尔值 true 和 false. 不接受其他类型的赋值,不支持类型转换。
var a bool
a = true
b := (2 == 3)
3.4 字符串
转义字符 | 含义 |
---|---|
\ | 表示反斜线 |
' | 单引号 |
" | 双引号 |
\n | 换行符 |
\uhhhh | 4 个 16 进制数字给定的 Unicode 字符 |
在Go语言中单个字符可以使用单引号' ' 来创建。
一个单一的字符可以用一个单一的rune来表示。 rune类型,等同于 int32。
字符串支持的操作如下:
语法 | 描述 |
---|---|
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 编码字节 |
3.5 格式化字符串
格式化指令 | 含义 |
---|---|
%% | % 字面量 |
%b | 一个二进制整数,将一个整数格式化为二进制的表达方式 |
%c | 一个 Unicode 的字符 |
%d | 十进制数值 |
%o | 八进制数值 |
%x | 小写的十六进制数值 |
%X | 大写的十六进制数值 |
%U | 一个 Unicode 表示法表示的整形码值,默认是 4 个数字字符 |
%s | 输出以原生的 UTF-8 字节表示的字符,如果 console 不支持 UTF-8 编码,则会输出乱码 |
%t | 以 true 或者 false 的方式输出布尔值 |
%v | 使用默认格式输出值,或者使用类型的 String() 方法输出的自定义值,如果该方法存在的话 |
%T | 输出值的类型 |
3.6 字符类型
Go语言中支持两个字符类型:
Byte (实际上是 Unit8的别名), 代表UTF-8字符串的单个字节的值。
rune , 代表单个 Unicode字符。
3.7 数组
数组是一个定长的序列,其中每个元素的类型都相同。 固定长度,不可修改。
var a [5] int
var b = [5]int{1,2,3,4,5}
var c = [...]int{1,2,3,4,5} // 如果使用了省略号,Go语言会为我们自动计算数组的长度。
3.8 切片
Go语言的切片比数组更加灵活、强大、方便! 切片不定长,可以随时调整长度。
数组是按值传递的(即传递的副本),而切片是引用类型,传递切片的成本非常小。
var a make([]int, 10, 20) // 切片的长度和容量分别为 10 和 20
var b = []int{1,2,3,4,5}
3.9 包
Go语言中的代码组织方式是包。 包是各种类型和函数的集合。
在包中,如果标识符的首字母是大写的,那这些标识符是可以被导出的,可以在包以外直接使用。
4. 流程控制
if aBoolValue {
block1
}else if anotherBoolValue {
block2
}else {
block3
}
go语言只支持for
循环语句,不支持 while / do-while
语句。
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
for boolVlaue {
// while 循环。 Go语言中没有while关键字
}
for index, char := range aString {
// 迭代字符串
}
for item := range aChannel {
// 迭代通道
}
switch aExpression {
case expression1:
block1
...
case expressionN:
blockN
default:
BlockD
}
5. 函数
package Divide
func divide(a int, b int) (num int, err error) {
if b == 0 {
err := errors.New("被除数不能为0")
return
}
return a / b, nil // 支持返回多个值。不必创建临时的结构体来包装。
}
// 下面是函数的调用
package main
import (
"Divide" // 导入 Add 包
"fmt"
)
func main() {
c, err := Divide.divide(1,2)
if err==nil {
fmt.Println(c)
}
}
5.1 匿名函数
匿名函数由一个不带函数名的函数声明和函数体组成。
func (a,b,c int) bool {
return a * b < c
}
你可以将匿名函数直接赋值给一个变量,也可以直接调用运行。
x := func (a,b,c int) bool {
return a * b < c
}
func (a,b,c int) bool {
return a * b < c
} (1,2,3) // 小括号内直接给参数列表 表示函数调用
接口是一个自定义类型。它声明了一个或者多个方法。任何实现了这些方法的对象(类型)都满足这个接口。
var i interface{} = 99 // 创建一个interface{}类型,其值为99
var s interface{} = []string{"left", "right"}
j := i.(int) // 我们假设i是兼容int类型,并使用类型断言将其转换为int类型
fmt.Printf("type and value of j is: %T and %d\n", j, j)
if s, ok := s.([]string); ok { // 创建了影子变量,if的作用域中覆盖了外部的变量s
fmt.Printf("%T -> %q\n", s, s)
}
5.2 defer
defer
语句是在 return
之后执行的。
func CopyFile(dst, src string) (w int64, err error) {
srcFile, err := os.Open(src)
if err != nil {
return
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return
}
defer dstFile.Close()
return io.Copy(dstFile, srcFile)
}
5.3 panic & recover
panic()
函数用于抛出异常,recover()
函数用于捕获异常。
6. 结构体
type ColorPoint struct {
color.Color // 匿名字段(嵌入)
x, y int // 具名字段(聚合)
}
7. 方法
方法是作用在自定义类型上的一类特殊函数。
定义方法和定义函数几乎相同,只是需要在 func
关键字和方法名之间必须写上接接受者。
type Count int
func (count *Count) Increment() { // 接受者是一个 `Count` 类型的指针
*count++
}
8. 组合
Go 语言中没有继承。 但是提供了一个组合特性。
相对于继承的编译期确定实现,组合的运行态指定实现,更加灵活。
9. 接口
Go中接口是一组方法签名。当一个类型为接口中的所有方法提供定义时,它被称为实现该接口。
infterface{}
类型是声明了空方法集的接口类型。任何一个值都满足 interface{}
类型,也就是说如果一个函数或者方法接收 interface{}
类型的参数,那么任意类型的参数都可以传递给该函数。
package main
import "fmt"
type Human struct { // 结构体
name string
age int
phone string
}
//Human实现SayHi方法
func (h Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Human实现Sing方法
func (h Human) Sing(lyrics string) {
fmt.Println("La la la la...", lyrics)
}
type Student struct {
Human //匿名字段
school string
loan float32
}
type Employee struct {
Human //匿名字段
company string
money float32
}
// Employee重载Human的SayHi方法
func (e Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone)
}
// Interface Men被Human,Student和Employee实现
// 因为这三个类型都实现了这两个方法
type Men interface {
SayHi()
Sing(lyrics string)
}
func main() {
mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
Tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}
//定义Men类型的变量i
var i Men
//i能存储Student
i = mike
fmt.Println("This is Mike, a Student:")
i.SayHi()
i.Sing("November rain")
//i也能存储Employee
i = Tom
fmt.Println("This is Tom, an Employee:")
i.SayHi()
i.Sing("Born to be wild")
//定义了slice Men
fmt.Println("Let's use a slice of Men and see what happens")
x := make([]Men, 3)
//这三个都是不同类型的元素,但是他们实现了interface同一个接口
x[0], x[1], x[2] = paul, sam, mike
for _, value := range x {
value.SayHi()
}
}
三、并发编程
1. 并发与并行
并发指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,通过 CPU 时间片轮转使多个进程快速交替的执行。而并行的关键是你有同时处理多个任务的能力。
并发和并行都可以是很多个线程,就看这些线程能不能同时被(多个)CPU 执行,如果可以就说明是并行,而并发是多个线程被(一个)CPU 轮流切换着执行。
并发与并行的区别:并发是两个队列,使用一台咖啡机;并行是两个队列,使用两台咖啡机。如果串行,一个队列使用一台咖啡机,那么哪怕前面那个人有事出去了半天,后面的人也只能等着他回来才能去接咖啡,这效率无疑是最低的。
2. 协程
协程也叫轻量级线程。与传统的进程和线程相比,协程最大的优点就在于其足够“轻”,操作系统可以轻松创建上百万个协程而不会导致系统资源枯竭,而线程和进程通常最多不过近万个。
多数语言在语法层面上是不支持协程的,一般都是通过库的方式进行支持,但库的支持方式和功能不够完善,经常会引发阻塞等一系列问题,而 Go 语言在语法层面上支持协程,也叫 goroutine
。这让协程变得非常简单,让轻量级线程的切换管理不再依赖于系统的进程和线程,也不依赖 CPU 的数量。
3. goroutine
goroutine
是 Go 语言并行设计的核心。goroutine
是一种比线程更轻量的实现,十几个 goroutine
可能在底层就是几个线程。
要使用 goroutine
只需要简单的在需要执行的函数前添加 go
关键字即可。
func Add(a, b int) {
c := a + b
fmt.Println(c)
}
go Add(1, 2) // 使用 go 关键字让函数并发执行
当在一个函数前加上 go
关键字,该函数就会在一个新的 goroutine 中并发执行,当该函数执行完毕时,这个新的 goroutine 也就结束了。
4. channel
Go 语言提供的信道(channel
)就是专门解决并发通信问题的.
channel
是goroutine
之间互相通讯的东西。类似我们 Unix 上的管道(可以在进程间传递消息),用来 goroutine
之间发消息和接收消息。其实,就是在做 goroutine
之间的内存共享。
var a chan int // 声明一个传递元素类型为int的channel
var b chan float64
var c chan string
x := make(chan int) // 声明并初始化一个int型的名为a的channel
y := make(chan float64)
z := make(chan string)
channel
最频繁的操作就是写入和读取,这两个操作也非常简单:
a := make(chan int)
a <- 1 // 将数据写入channel
z := <-a // 从channel中读取数据
channel
的关闭非常简单,使用 Go 语言内置的 close() 函数即可关闭 channel
ch := make(chan int)
close(ch)
5. select
select
用于处理异步 IO 问题,它的语法与 switch
非常类似。由 select
开始一个新的选择块,每个选择条件由 case
语句来描述,并且每个 case
语句里必须是一个 channel
操作。
func main() {
c1 := make(chan string) // 初始化两个 channel c1 和 c2
c2 := make(chan string)
go func() { // 开启两个 goroutine 分别往 c1 和 c2 写入数据
time.Sleep(time.Second * 1)
c1 <- "one"
}()
go func() {
time.Sleep(time.Second * 2)
c2 <- "two"
}()
for i := 0; i < 2; i++ {
select { // 通过 select 监听两个 channel
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}
6. 超时机制
t := make(chan bool)
go func {
time.Sleep(1e9) //等待1秒
t <- true
}
select {
case <-ch: //从ch中读取数据
case <-t: //如果1秒后没有从ch中读取到数据,那么从t中读取,并进行下一步操作
}
这样的方法就可以让程序在等待 1 秒后继续执行,而不会因为 ch 读取等待而导致程序停滞,从而巧妙地实现了超时处理机制,这种方法不仅简单,在实际项目开发中也是非常实用的。