环境安装

  • $GOROOT 是 go 的安装路径
  • $GOPATH 是自己使用 go 进行开发的工作路径
  • $GOPATH/bin 是存放 go install 下载的依赖包

    Mac

    注意,brew 工具依赖于 ruby,所以要提前安装好 ruby 环境再进行 brew 工具的安装。 使用 brew 或者 wget 工具安装 Go,。安装完之后解压到一个自己喜欢的目录(该目录之后就是GOROOT), vi ~/.zshrc 更改环境变量,添加
    1. export GOROOT=go安装路径
    2. export GOPATH=自己定一个GOPATH
    3. export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

    Linux

    使用 brew 或者 wget 命令下载好 go 之后,解压到一个自己喜欢的目录(该目录之后就作为 GOROOT了)。接着设置环境变量,vi $HOME/.profile,添加
    1. export GOROOT=go安装路径
    2. export GOPATH=自己定一个GOAPTH
    3. epoxrt PATH=$PATH:$GOROOT/bin:$GOPATH/bin

    module 相关

    全局开启 module 的时候,创建项目无需受 $GOPATH/src 格式的约束,参数如下
    1. go env -w GO111MODULE=on # 全局启用 module,无需 $GOPATH/src 格式的束缚
    2. # auto 默认值,在 $GOPATH/src 中不开启 module
    3. # off 关闭 module 模式

    变量导出

    方法名开头大写,则方法为导出方法;否则方法为包内私有

module 依赖

不同模块的依赖:

  • 本地未发布模块```go module hello

go 1.14

// 使用关键字 replace, go build 之后,会使用本地路径来引用模块 replace example.com/greetings => ../greetings

  1. - 已发布的包```go
  2. require example.com/greetings v1.1.0

执行

  1. func init() {...}

go 会在全局变量初始化完后,执行 init() 函数

Basic

变量声明

方法内部,可以使用 := 的简版声明变量方式,可以由编译器自动推断类型;在方法外,每一个声明都必须使用关键字(var func),不能使用简版声明方式

数据类型

基本类型

  1. bool
  2. string
  3. int int8 int16 int32 int64
  4. uint uint8 uint16 uint32 uint64 uintptr
  5. byte // alias for uint8
  6. rune // alias for int32
  7. // represents a Unicode code point
  8. float32 float64
  9. complex64 complex128

零值

  • 0,数值类型
  • false,布尔类型
  • "",string类型
  • nil,指针类型

对于 struct 结构体而言,其默认零值就是一个可访问的结构体,其所有字段都其默认零值。

类型转换

  • 显式类型转换,T(x)

常量

使用 const 关键字声明,不能够使用 := 的方式创建

指针

基本同 C 语言,但是 Go 没有指针运算,即指针仅用于索引内存的值

  1. func main() {
  2. arr := [...]int{1,2,3,4}
  3. p := &arr
  4. // fmt.Println(*p) // [1 2 3 4]
  5. fmt.Println(*(p + 1)) // 报错!
  6. }

结构体 struct

和 C 语言基本一致,区别在于结构体指针 p 仍然可以使用 p.X 的方式访问字段,这是 Go 对语法的简化。

Array Slice

数组定长,切片是数组的一个窗口引用,描述的了底层数组的一个片段,切片的所有操作本质上改变的是底层的数组。所以多个切片引用一个数组的时候,改动相互之间可见。

Golang 基础 - 图1

  1. names := [...]int{1, 2, 3, 4, 5, 6, 7}
  2. a := names[0: 4]
  3. b := names[2: 4] // 两者引用自同一个数组,只是引用片段有区别
  1. [3]bool{true, true, false} // 创建一个数组
  2. []bool{true, true, false} // 创建一个数组,再创建切片指向数组

slice 有两个长度属性,len就是当前引用窗口的大小cap就是底层数组的大小(从切片下界开始计算)。

  1. func main() {
  2. arr := []int{1,2,3,4,5,6,7}
  3. s := arr[:4]
  4. fmt.Println(len(s)) // 4
  5. fmt.Println(cap(s)) // 7
  6. }

注意点

与面向对象的 Java 不同,Golang 中的 array 在函数传参的时候穿的是值。slice 在函数传参的时候也是传值,由于 slice 本身就是引用,所以操作会影响底层的数组。

创建切片

  • 从数组创建切片
  • 使用make([]Type, len, cap)

切片操作

  • 末尾添加go func append(s []T, vs ...T) []T // 返回新的切片引用

  • range 关键字可以作用于数组、切片,返回 idx, val或者只需获取`val`go for i, val := range s { // ... } for val := range s { // ... }

Map

存储 K-V 键值对。

map 操作

  • 创建mapgo m := make(map[string]int)

  • 修改值go m[key] = val

  • 删除keygo delete(m, key)

  • 获取值,key不存在的时候,获取的 val 是该类型的默认零值go val = m[key] val, ok := m[key] // 若key不存在,则ok=false, val为零值

控制流程

循环 For

和其他语言的类似,但是 Go 中没有 while 关键字,一切循环都使用 for 关键字

  1. for sum < 1000 {
  2. sum += sum
  3. }
  4. for { // 不停循环,和 while(true) 一样
  5. }

range

range 关键字返回可迭代类型的 KV 键值对。可迭代类型包括 map、array、string、slice,其中 array、string、slice 返回的 K 键值对的 K 就是从0开始的下标值。

在使用 range 遍历 string 的时候,被切分出来的是 rune 类型的一个符文。rune 是什么意思呢?在 Golang 中,string 底层是使用 byte 数组实现的。对于非英文而言就会有些问题,例如中文在 unicode 下是2个字节,而在 utf-8 中是3个字节(Golang 的默认编码是 utf-8)。那么想知道一个字符串中有多少个字面可见的字符就不能够使用 len(s) 来做到,因为这个计算的是字节数。

  • byte 等同于 uint8,用于处理 ascii 字符
  • rune 等同与 int32,用于处理 unicode 或者 utf-8 字符

看一个例子来具体区分 rune 和 char

  1. s := "hi 哈哈"
  2. fmt.Println(len(s)) // 输出9,统计的是字符串的总 字节
  3. fmt.Println(utf8.RuneCountInString(s)) // 输出5,统计的是字符串的总 字符元素

条件判断 if,switch

if 和 switch 都可以使用一个简短声明来创建一个变量,且 switch 的 break 可忽略不写。

  1. func pow(x, n, lim float64) {
  2. if v := math.Pow(x, n); v < lim {
  3. return v
  4. }
  5. return lim
  6. }
  7. func main() {
  8. switch os := runtime.GOOS; os {
  9. case "darwin":
  10. fmt.Println("OS X.")
  11. case "linux":
  12. fmt.Println("Linux.")
  13. default:
  14. fmt.Printf("%s.\n", os)
  15. }
  16. }

switch 还可以不写判断的变量,直接变成了 if-else 的相同作用

  1. func main() {
  2. t := time.Now()
  3. switch {
  4. case t.Hour() < 12:
  5. fmt.Println("Morning")
  6. case t.Hour() < 17:
  7. fmt.Println("Afternoon")
  8. default:
  9. fmt.Println("Evening")
  10. }
  11. }

Defer

总而言之就是在当前函数完全退出之前必然执行 defer 之后的语句。但是这要保证 defer 语句本身被执行,所以 defer 的放置位置很重要,尽量避免相隔多行之后进行处理,保证 defer 之后的语句入栈(LIFO 式调用执行)。

Defer 中涉及返回值的细节

在有返回值的函数中,使用 defer 后的函数完成顺序如下:

  1. 确定返回的值
  2. 执行 defer 后的语句
  3. 执行 return 退出函数

有名返回值情况

  1. func testDefer() (res int) {
  2. i := 1
  3. defer func() {
  4. res++
  5. }()
  6. return i
  7. }

对于这个例子:

  • 首先进入到函数中后,res 被赋上默认值 0
  • 执行到 defer 则先将之后的 func 注册到栈中
  • 接着执行到 return 时,先确定返回值,即 res=i,res 此时的值变成了 1
  • 执行 defer 后的语句,res++,res 值变成 2
  • 执行 return 退出函数,所以最终的返回值是 2

无名返回值的情况可以看作是,系统自己创建了一个返回值的名字

  1. func testDefer() int {
  2. i := 1
  3. defer func() {
  4. i++
  5. }()
  6. return i
  7. }
  8. // 等价于
  9. func testDefer() int {
  10. i := 1
  11. defer func() {
  12. i++
  13. }()
  14. res := i
  15. return res
  16. }

函数

闭包

函数方面和 JS 很类似,可以传递回调函数,所以在 Go 中也有闭包的概念。闭包就是子函数引用的父函数变量集合,然后使用者使用这个子函数控制其引用的父函数变量。

类型赋予方法

Go 语言中没有类,但是可以给所有类型附加方法

  1. type MyType struct {
  2. X, Y int
  3. }
  4. func (v MyType) Add() int { // 将 Add 方法附加给 MyType 类型
  5. return v.X + v.Y
  6. }

上述方法获取的是方法接收者的值拷贝进行操作,若要改变原本的接收者值,接收者需要使用指针类型

  1. type MyString string
  2. func (v *MyString) Smile() { // 将 Add 方法,附加给 *MyString 类型
  3. *v += " smile:)"
  4. }

接口

接口就是方法声明的集合。在类型实现接口的时候,无需指明接口名,只需将接口的所有方法实现并赋值给实现类型。普通接收者实现,则该类型实现了接口;指针接收者实现,则该类型指针实现了接口。

  1. type ITest interface {
  2. SayHello() string
  3. }
  4. type HerString string
  5. func (s *HerString) SayHello() string { // *HerString 实现接口
  6. return "Hello yeah"
  7. }
  8. func main() {
  9. hs := HerString("")
  10. // var t ITest = hs 报错
  11. var t ITest = &hs
  12. fmt.Println(t.SayHello())
  13. }
  1. type ITest interface {
  2. SayHello() string
  3. }
  4. type MyString string
  5. func (s MyString) SayHello() string { // MyString 实现接口
  6. return "Hello"
  7. }
  8. func main() {
  9. ms := MyString("")
  10. var t ITest = ms
  11. fmt.Println(t.SayHello())
  12. }

接口原理

接口值可以看作是一个值、具体类型 (value, concreteType) 形式的元组,调用一个接口的方法调用的就是其具体类型的方法,这种多态的思想和 C++/Java 是一致的。

  1. package main
  2. type I interface {
  3. M()
  4. }
  5. type T struct {
  6. S string
  7. }
  8. func (t *T) M() {
  9. // ...
  10. }
  11. func main() {
  12. var i I
  13. i = &T{"Hello"}
  14. fmt.Printf("%v, %T", i, i) // &{Hello}, *main.T
  15. }

nil 值接口

在 Go 中空指针也可以调用接口的方法,因为接口的变量一旦声明,其本身就可以看作是一个非空元组(<nil>, <nil>),只不过值为 nil,具体实现类型为 nil

一旦指定了实现类型(<nil>, concreteType) 就算值为 nil 仍然可以进行接口方法调用。

没有指定实现类型,调用方法报错!

  1. type I interface {
  2. M()
  3. }
  4. type T struct {
  5. S string
  6. }
  7. func (t *T) M() {
  8. if t == nil {
  9. fmt.Println("<nil>")
  10. return
  11. }
  12. fmt.Println(t.S)
  13. }
  14. func main() {
  15. var i I
  16. // i.M() // 报错
  17. var t *T // t == <nil>
  18. i = t
  19. i.M() // "<nil>"
  20. }

空接口

空接口 interface{} 可以拥有任何类型的值,即任何类型都实现了空接口

  1. package main
  2. import "fmt"
  3. func main() {
  4. var i interface{}
  5. describe(i) // (<nil>,<nil>)
  6. i = 42
  7. describe(i) // (42, int)
  8. }
  9. func describe(i interface{}) {
  10. fmt.Printf("(%v, %T)\n", i, i)
  11. }

类型断言

通过类型断言,可以在获取接口值的时候检查其具体类型,即获取到元组 (val, type) 中的 type。

  1. // i 的具体类型不是T时 panic
  2. t := i.(T)
  3. // i 的具体类型不是T时,ok=false,t为T类型零值
  4. // 和 map 类似
  5. t, ok := i.(T)
  1. var i interface{} = "Hello"
  2. s, ok := i.(string)
  3. fmt.Println(s, ok) // Hello, true

Type switch

使用关键字 type 可以结合 switch 进行类型分支判断

  1. switch v := i.(type) {
  2. case T:
  3. // v has type T
  4. case S:
  5. // v has type S
  6. default:
  7. // no match
  8. }

常用接口

Stringer

打印某个类型的时候 fmt 通过 String() 方法获取值,类似 Java 中的 toString。

  1. type Stringer interface {
  2. String() string
  3. }

Error

错误类型,打印错误的时候 fmt 包通过 Error 接口获取值。

  1. type error interface {
  2. Error() string
  3. }

注意:在实现 Error 方法时,若要在其中打印值本身,要将该值转换成非error类型,否则会导致无限循环。

Reader

io.Reader 接口声明用于读取字节流的方法,标准库中有许多对这个接口的实现。其工作流程如下:

  • 将字节流读入字节切片 p
  • 返回读取到的字节数 n
  • 当流结束时,返回 io.EOF
  1. type Reader interface {
  2. Read(p []byte) (n int, err error)
  3. }

例子:strings.Reader 读取 string

  1. func main() {
  2. r := strings.NewReader("Hello Reader!")
  3. b := make([]byte, 8) // 每次最多读入8byte
  4. for {
  5. n, err := r.Read(b)
  6. fmt.Printf("b[:n] = %q\n", b[:n])
  7. if err == io.EOF {
  8. break
  9. }
  10. }
  11. }

Image

图片接口描述的实际是有限的颜色方形颜色块

  1. type Image interface {
  2. ColorModel() color.Model // 默认为 color.RGBAModel
  3. Bounds() Rectangle
  4. At(x, y int) color.Color // 默认为 color.RGBA
  5. }

并发

协程

更加轻量级的线程,多个协程执行在同一个地址空间。也可以看作是每一个线程执行的任务,线程可以主动切换任务执行,减少了上下文切换、cache 同步的开销。

Golang 基础 - 图2

具体看看进程、线程、协程的比较

进程 线程 协程
CPU - 线程是 CPU 调度的基本单位,OS 负责分配线程到 CPU 上进行执行 运行在线程上
内存 进程是资源分配的基本单位,进程拥有自己独立的内存空间 多个线程共享使用进程的内存空间 同样是使用进程的空间
- 一个运行时的方法产生一个栈帧,OS 分配的调用栈大小默认为 8MB 用户程序在上保存每一个协程栈的信息,切换的时候修改相关寄存器的指针(SP栈顶,BP栈底,PC)
切换方式 - 触发中断,内核负责切换线程 主动 yield 切换,让出 CPU,在用户态完成
切换内容 通用寄存器
PC 寄存器
内核栈
CPU 缓存信息
页表寄存器
TLB 缓存
通用寄存器
PC 寄存器
内核栈
CPU 缓存信息
通用寄存器
PC 寄存器

所以协程的切换其实就类似当前线程在执行代码的时候,突然跳到另一个部分的代码进行执行,主要需要修改栈帧寄存器 BP 和 SP 的还有 PC 寄存器的指向即可。

Golang 基础 - 图3

Goroutine

goroutine 是由 Go runtime 管理的轻量级线程(本质为协程),所以不同多个 goroutines 运行在相同的地址空间中,对于共享内容需要进行同步互斥。

在方法前使用关键字 go,则该方法在一个新的 goroutine 中执行,当前 goroutine 继续执行当前方法内容。

  1. func say(s string) {
  2. fmt.Println(s)
  3. }
  4. func main() {
  5. go say("hahaha")
  6. say("wowow")
  7. }

Channel

一种传输特定类型的半双工管道,默认情况下,管道缓冲区只能存放一个元素,其半双工性质有利于用于在多协程环境下协程之间的同步通信。注意,在使用的时候 channel 缓冲为空会阻塞读,channel 缓冲满会阻塞写。

使用关键字 chan

  1. ch <- v // 将 v 的值送入管道
  2. v := <- ch // 将管道的内容输出到 v
  3. ch := make(chan int) // 创建一个传输 int 类型的管道
  1. func sum(s []int, c chan int) {
  2. sum := 0
  3. for _, v := range s {
  4. sum += v
  5. }
  6. c <- sum // send sum to c
  7. }
  8. func main() {
  9. s := []int{7, 2, 8, -9, 4, 0}
  10. c := make(chan int)
  11. go sum(s[: len(s) / 2], c)
  12. go sum(s[len(s) / 2:], c)
  13. x, y := <-c, <-c // 从左到右执行赋值
  14. fmt.Println(x, y, x+y) // -5, 17, 12
  15. }

Buffered Channel

默认情况下channel只能存储一个元素,可以在创建 channel 时显式指定缓冲区存储元素的个数,这样在缓冲区满的时候就会阻塞写。

  1. // 创建一个 channel,其缓冲区能够存储 100 个 int
  2. ch := make(chan int, 100)

Range and Close

channel 可以被 for 循环遍历,由于 channel 是动态地获取数据的,所以要关闭 channel 才能够在遍历的时候正确停止,否则一直阻塞等待导致死锁。

一般情况下很少会关闭 channel。

  1. func fibonacci(n int, c chan int) {
  2. x, y := 0, 1
  3. for i := 0; i < n; i++ {
  4. c <- x
  5. x, y = y, x+y
  6. }
  7. close(c) // 关闭管道
  8. }
  9. func main() {
  10. c := make(chan int, 100)
  11. go fibonacci(10, c)
  12. for i := range c { // 打印前 10 个数
  13. fmt.Println(i)
  14. }
  15. }

Select

select 表达式配合管道使用,让当前 goroutine 阻塞等待分支语句,直到其中的一个语句分支能够执行。

  1. func fibonacci(c, quit chan int) {
  2. x, y := 0, 1
  3. for {
  4. select {
  5. case c <- x: // 往管道中输入数据
  6. x, y = y, x+y
  7. case <-quit: // quit 中没有数据的时候阻塞
  8. fmt.Println("quit")
  9. return
  10. // default: 也可以使用一个 default 情况,使得循环不阻塞
  11. }
  12. }
  13. }
  14. func main() {
  15. c := make(chan int)
  16. quit := make(chan int)
  17. go func() {
  18. for i := 0; i < 10; i++ {
  19. fmt.Println(<-c) // c 中没有数据时,读阻塞
  20. }
  21. quit <- 0
  22. }()
  23. fibonacci(c, quit)
  24. }

sync.Mutex

互斥锁,用于框定互斥操作,包含两个方法。其中解锁方法可以使用 defer 关键字标识,保证其成功执行。

  • Lock
  • Unlock
  1. type SafeCounter struct {
  2. m sync.Mutext
  3. v map[string]int
  4. }
  5. func (c *SafeCounter) Incr(key string) {
  6. c.m.Lock()
  7. c.v[key]++ // 临界区,单线程访问
  8. defer c.m.Unlock()
  9. }
  10. func (c *SafeCounter) Value(key string) {
  11. c.m.Lock()
  12. defer c.m.Unlock()
  13. return c.v[key]
  14. }
  15. func main() {
  16. c := SafeCounter{v: make(map[string]int)}
  17. for i := 0; i < 1000; i++ {
  18. go c.Incr("a key")
  19. }
  20. time.Sleep(time.Second)
  21. fmt.Println(c.Value("a key"))
  22. }