环境安装
- $GOROOT 是 go 的安装路径
- $GOPATH 是自己使用 go 进行开发的工作路径
- $GOPATH/bin 是存放 go install 下载的依赖包
Mac
注意,brew 工具依赖于 ruby,所以要提前安装好 ruby 环境再进行 brew 工具的安装。 使用 brew 或者 wget 工具安装 Go,。安装完之后解压到一个自己喜欢的目录(该目录之后就是GOROOT), vi ~/.zshrc 更改环境变量,添加export GOROOT=go安装路径
export GOPATH=自己定一个GOPATH
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
Linux
使用 brew 或者 wget 命令下载好 go 之后,解压到一个自己喜欢的目录(该目录之后就作为 GOROOT了)。接着设置环境变量,vi $HOME/.profile,添加export GOROOT=go安装路径
export GOPATH=自己定一个GOAPTH
epoxrt PATH=$PATH:$GOROOT/bin:$GOPATH/bin
module 相关
全局开启 module 的时候,创建项目无需受 $GOPATH/src 格式的约束,参数如下go env -w GO111MODULE=on # 全局启用 module,无需 $GOPATH/src 格式的束缚
# auto 默认值,在 $GOPATH/src 中不开启 module
# off 关闭 module 模式
变量导出
方法名开头大写,则方法为导出方法;否则方法为包内私有
module 依赖
不同模块的依赖:
- 本地未发布模块```go module hello
go 1.14
// 使用关键字 replace, go build 之后,会使用本地路径来引用模块 replace example.com/greetings => ../greetings
- 已发布的包```go
require example.com/greetings v1.1.0
执行
func init() {...}
go 会在全局变量初始化完后,执行 init() 函数
Basic
变量声明
方法内部,可以使用 :=
的简版声明变量方式,可以由编译器自动推断类型;在方法外,每一个声明都必须使用关键字(var
func
),不能使用简版声明方式
数据类型
基本类型
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
// represents a Unicode code point
float32 float64
complex64 complex128
零值
0
,数值类型false
,布尔类型""
,string类型nil
,指针类型
对于 struct
结构体而言,其默认零值就是一个可访问的结构体,其所有字段都其默认零值。
类型转换
- 显式类型转换,
T(x)
常量
使用 const
关键字声明,不能够使用 :=
的方式创建
指针
基本同 C 语言,但是 Go 没有指针运算,即指针仅用于索引内存的值
func main() {
arr := [...]int{1,2,3,4}
p := &arr
// fmt.Println(*p) // [1 2 3 4]
fmt.Println(*(p + 1)) // 报错!
}
结构体 struct
和 C 语言基本一致,区别在于结构体指针 p
仍然可以使用 p.X
的方式访问字段,这是 Go 对语法的简化。
Array Slice
数组定长,切片是数组的一个窗口引用,描述的了底层数组的一个片段,切片的所有操作本质上改变的是底层的数组。所以多个切片引用一个数组的时候,改动相互之间可见。
names := [...]int{1, 2, 3, 4, 5, 6, 7}
a := names[0: 4]
b := names[2: 4] // 两者引用自同一个数组,只是引用片段有区别
[3]bool{true, true, false} // 创建一个数组
[]bool{true, true, false} // 创建一个数组,再创建切片指向数组
slice 有两个长度属性,len
就是当前引用窗口的大小,cap
就是底层数组的大小(从切片下界开始计算)。
func main() {
arr := []int{1,2,3,4,5,6,7}
s := arr[:4]
fmt.Println(len(s)) // 4
fmt.Println(cap(s)) // 7
}
注意点
与面向对象的 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 操作
创建map
go m := make(map[string]int)
修改值
go m[key] = val
删除key
go delete(m, key)
获取值,key不存在的时候,获取的 val 是该类型的默认零值
go val = m[key] val, ok := m[key] // 若key不存在,则ok=false, val为零值
控制流程
循环 For
和其他语言的类似,但是 Go 中没有 while 关键字,一切循环都使用 for
关键字
for sum < 1000 {
sum += sum
}
for { // 不停循环,和 while(true) 一样
}
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
s := "hi 哈哈"
fmt.Println(len(s)) // 输出9,统计的是字符串的总 字节
fmt.Println(utf8.RuneCountInString(s)) // 输出5,统计的是字符串的总 字符元素
条件判断 if,switch
if 和 switch 都可以使用一个简短声明来创建一个变量,且 switch 的 break 可忽略不写。
func pow(x, n, lim float64) {
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}
func main() {
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
fmt.Printf("%s.\n", os)
}
}
switch 还可以不写判断的变量,直接变成了 if-else 的相同作用
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Morning")
case t.Hour() < 17:
fmt.Println("Afternoon")
default:
fmt.Println("Evening")
}
}
Defer
总而言之就是在当前函数完全退出之前必然执行 defer 之后的语句。但是这要保证 defer 语句本身被执行,所以 defer 的放置位置很重要,尽量避免相隔多行之后进行处理,保证 defer 之后的语句入栈(LIFO 式调用执行)。
Defer 中涉及返回值的细节
在有返回值的函数中,使用 defer 后的函数完成顺序如下:
- 确定返回的值
- 执行 defer 后的语句
- 执行 return 退出函数
有名返回值情况
func testDefer() (res int) {
i := 1
defer func() {
res++
}()
return i
}
对于这个例子:
- 首先进入到函数中后,res 被赋上默认值 0
- 执行到 defer 则先将之后的 func 注册到栈中
- 接着执行到 return 时,先确定返回值,即 res=i,res 此时的值变成了 1
- 执行 defer 后的语句,res++,res 值变成 2
- 执行 return 退出函数,所以最终的返回值是 2
无名返回值的情况可以看作是,系统自己创建了一个返回值的名字
func testDefer() int {
i := 1
defer func() {
i++
}()
return i
}
// 等价于
func testDefer() int {
i := 1
defer func() {
i++
}()
res := i
return res
}
函数
闭包
函数方面和 JS 很类似,可以传递回调函数,所以在 Go 中也有闭包的概念。闭包就是子函数引用的父函数变量集合,然后使用者使用这个子函数控制其引用的父函数变量。
类型赋予方法
Go 语言中没有类,但是可以给所有类型附加方法。
type MyType struct {
X, Y int
}
func (v MyType) Add() int { // 将 Add 方法附加给 MyType 类型
return v.X + v.Y
}
上述方法获取的是方法接收者的值拷贝进行操作,若要改变原本的接收者值,接收者需要使用指针类型。
type MyString string
func (v *MyString) Smile() { // 将 Add 方法,附加给 *MyString 类型
*v += " smile:)"
}
接口
接口就是方法声明的集合。在类型实现接口的时候,无需指明接口名,只需将接口的所有方法实现并赋值给实现类型。普通接收者实现,则该类型实现了接口;指针接收者实现,则该类型指针实现了接口。
type ITest interface {
SayHello() string
}
type HerString string
func (s *HerString) SayHello() string { // *HerString 实现接口
return "Hello yeah"
}
func main() {
hs := HerString("")
// var t ITest = hs 报错
var t ITest = &hs
fmt.Println(t.SayHello())
}
type ITest interface {
SayHello() string
}
type MyString string
func (s MyString) SayHello() string { // MyString 实现接口
return "Hello"
}
func main() {
ms := MyString("")
var t ITest = ms
fmt.Println(t.SayHello())
}
接口原理
接口值可以看作是一个值、具体类型 (value, concreteType)
形式的元组,调用一个接口的方法调用的就是其具体类型的方法,这种多态的思想和 C++/Java 是一致的。
package main
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
// ...
}
func main() {
var i I
i = &T{"Hello"}
fmt.Printf("%v, %T", i, i) // &{Hello}, *main.T
}
nil 值接口
在 Go 中空指针也可以调用接口的方法,因为接口的变量一旦声明,其本身就可以看作是一个非空元组(<nil>, <nil>)
,只不过值为 nil
,具体实现类型为 nil
。
一旦指定了实现类型,(<nil>, concreteType)
就算值为 nil
仍然可以进行接口方法调用。
没有指定实现类型,调用方法报错!
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
func main() {
var i I
// i.M() // 报错
var t *T // t == <nil>
i = t
i.M() // "<nil>"
}
空接口
空接口 interface{}
可以拥有任何类型的值,即任何类型都实现了空接口。
package main
import "fmt"
func main() {
var i interface{}
describe(i) // (<nil>,<nil>)
i = 42
describe(i) // (42, int)
}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
类型断言
通过类型断言,可以在获取接口值的时候检查其具体类型,即获取到元组 (val, type)
中的 type。
// i 的具体类型不是T时 panic
t := i.(T)
// i 的具体类型不是T时,ok=false,t为T类型零值
// 和 map 类似
t, ok := i.(T)
var i interface{} = "Hello"
s, ok := i.(string)
fmt.Println(s, ok) // Hello, true
Type switch
使用关键字 type
可以结合 switch 进行类型分支判断
switch v := i.(type) {
case T:
// v has type T
case S:
// v has type S
default:
// no match
}
常用接口
Stringer
打印某个类型的时候 fmt
通过 String()
方法获取值,类似 Java 中的 toString。
type Stringer interface {
String() string
}
Error
错误类型,打印错误的时候 fmt
包通过 Error 接口获取值。
type error interface {
Error() string
}
注意:在实现 Error 方法时,若要在其中打印值本身,要将该值转换成非error类型,否则会导致无限循环。
Reader
io.Reader
接口声明用于读取字节流的方法,标准库中有许多对这个接口的实现。其工作流程如下:
- 将字节流读入字节切片 p
- 返回读取到的字节数 n
- 当流结束时,返回
io.EOF
type Reader interface {
Read(p []byte) (n int, err error)
}
例子:strings.Reader
读取 string
func main() {
r := strings.NewReader("Hello Reader!")
b := make([]byte, 8) // 每次最多读入8byte
for {
n, err := r.Read(b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}
Image
图片接口描述的实际是有限的颜色方形颜色块
type Image interface {
ColorModel() color.Model // 默认为 color.RGBAModel
Bounds() Rectangle
At(x, y int) color.Color // 默认为 color.RGBA
}
并发
协程
更加轻量级的线程,多个协程执行在同一个地址空间。也可以看作是每一个线程执行的任务,线程可以主动切换任务执行,减少了上下文切换、cache 同步的开销。
具体看看进程、线程、协程的比较
进程 | 线程 | 协程 | |
---|---|---|---|
CPU | - | 线程是 CPU 调度的基本单位,OS 负责分配线程到 CPU 上进行执行 | 运行在线程上 |
内存 | 进程是资源分配的基本单位,进程拥有自己独立的内存空间 | 多个线程共享使用进程的内存空间 | 同样是使用进程的空间 |
栈 | - | 一个运行时的方法产生一个栈帧,OS 分配的调用栈大小默认为 8MB | 用户程序在堆上保存每一个协程栈的信息,切换的时候修改相关寄存器的指针(SP栈顶,BP栈底,PC) |
切换方式 | - | 触发中断,内核负责切换线程 | 主动 yield 切换,让出 CPU,在用户态完成 |
切换内容 | 通用寄存器 PC 寄存器 内核栈 CPU 缓存信息 页表寄存器 TLB 缓存 |
通用寄存器 PC 寄存器 内核栈 CPU 缓存信息 |
通用寄存器 PC 寄存器 |
所以协程的切换其实就类似当前线程在执行代码的时候,突然跳到另一个部分的代码进行执行,主要需要修改栈帧寄存器 BP 和 SP 的还有 PC 寄存器的指向即可。
Goroutine
goroutine 是由 Go runtime 管理的轻量级线程(本质为协程),所以不同多个 goroutines 运行在相同的地址空间中,对于共享内容需要进行同步互斥。
在方法前使用关键字 go
,则该方法在一个新的 goroutine 中执行,当前 goroutine 继续执行当前方法内容。
func say(s string) {
fmt.Println(s)
}
func main() {
go say("hahaha")
say("wowow")
}
Channel
一种传输特定类型的半双工管道,默认情况下,管道缓冲区只能存放一个元素,其半双工性质有利于用于在多协程环境下协程之间的同步通信。注意,在使用的时候 channel 缓冲为空会阻塞读,channel 缓冲满会阻塞写。
使用关键字 chan
ch <- v // 将 v 的值送入管道
v := <- ch // 将管道的内容输出到 v
ch := make(chan int) // 创建一个传输 int 类型的管道
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[: len(s) / 2], c)
go sum(s[len(s) / 2:], c)
x, y := <-c, <-c // 从左到右执行赋值
fmt.Println(x, y, x+y) // -5, 17, 12
}
Buffered Channel
默认情况下channel只能存储一个元素,可以在创建 channel 时显式指定缓冲区存储元素的个数,这样在缓冲区满的时候就会阻塞写。
// 创建一个 channel,其缓冲区能够存储 100 个 int
ch := make(chan int, 100)
Range and Close
channel 可以被 for 循环遍历,由于 channel 是动态地获取数据的,所以要关闭 channel 才能够在遍历的时候正确停止,否则一直阻塞等待导致死锁。
一般情况下很少会关闭 channel。
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c) // 关闭管道
}
func main() {
c := make(chan int, 100)
go fibonacci(10, c)
for i := range c { // 打印前 10 个数
fmt.Println(i)
}
}
Select
select 表达式配合管道使用,让当前 goroutine 阻塞等待分支语句,直到其中的一个语句分支能够执行。
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x: // 往管道中输入数据
x, y = y, x+y
case <-quit: // quit 中没有数据的时候阻塞
fmt.Println("quit")
return
// default: 也可以使用一个 default 情况,使得循环不阻塞
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c) // c 中没有数据时,读阻塞
}
quit <- 0
}()
fibonacci(c, quit)
}
sync.Mutex
互斥锁,用于框定互斥操作,包含两个方法。其中解锁方法可以使用 defer 关键字标识,保证其成功执行。
Lock
Unlock
type SafeCounter struct {
m sync.Mutext
v map[string]int
}
func (c *SafeCounter) Incr(key string) {
c.m.Lock()
c.v[key]++ // 临界区,单线程访问
defer c.m.Unlock()
}
func (c *SafeCounter) Value(key string) {
c.m.Lock()
defer c.m.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Incr("a key")
}
time.Sleep(time.Second)
fmt.Println(c.Value("a key"))
}