Go
go
是编译型语言。go
的工具链将程序的源文件转变成机器相关的原生二进制指令
go run
它将一个或多个.go
为后缀的源文件进行编译,链接,然后运行生成的可执行文件
go build
生成一个二进制程序,可以不用进行任何其他处理,随时执行
printf
verb | description |
---|---|
%d | 十进制整数 |
%x, %o, %b | 十六进制、八进制、二进制整数 |
%f, %g, %e | 浮点数: 如3.141593, 3.141592653589793, 3.141593e+00 |
%t | 布尔型:true 或 false |
%c | 字符(Unicode码点) |
%s | 字符串 |
%q | 带引号的字符串(如"abc")或者字符(如’c’) |
%v | 内置格式的任何值 |
%T | 任何值的类型 |
%% | 百分号本身, 无操作数 |
- 并发获取多个url
通道是一种允许某一例程向另一个例程传递指定类型的值的通信机制。
main
函数在一个goroutine
中执行,然后go语句创建额外的goroutine
当一个
goroutine
试图在一个通道上进行发送或接受操作时,它会阻塞,直到另一个goroutine
试图进行接收或发送操作才传递值, 并开始处理两个goroutine
package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"time"
)
func main() {
start := time.Now()
ch := make(chan string)
for _, url := range os.Args[1:] {
go fetch(url, ch)
}
for range os.Args[1:] {
fmt.Println(<-ch)
}
fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}
func fetch(url string, ch chan<- string) {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprint(err)
return
}
bytes, err := io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close() //防止资源泄露
if err != nil {
ch <- fmt.Sprintf("while reading %s: %v", url, err)
return
}
secs := time.Since(start).Seconds()
ch <- fmt.Sprintf("%.2fs %7d %s", secs, bytes, url)
}
new
每一次调用
new
返回一个具有唯一地址的不同变量例外:
struct{}, [0]int
(拥有相同的地址)
p := new(int) // 0
*p = 2 // 2
- 两个整数的最大公约数
func gcd(x, y int) int {
for y != 0 {
x,y = y, x%y
}
return x
}
- 斐波那契第n个数
func fib(n int) int {
x, y := 0, 1
for i := 0; i < n; i++ {
x, y = y, x+y
}
return x
}
取模
// 取模余数的正负号总是与被除数一致
-5 % 3 // -2
-5 % -3 // -2
// 除法运算取决于操作数是否为整数,正数会舍弃小数部分
5.0 / 4.0 // 1.25
5 / 4 // 1
- 类型转换
浮点数转整形,会舍弃小数部分,趋零结尾(正值向下取整,负值向上取整)
- 布尔值
无法隐式转换成数值
// b to i
func btoi(b bool) int {
if b {
return 1
}
return 0
}
// i to b
func itob(i int) bool { return i != 0 }
转换
x, err := strconv.Itoa(123) # Interger to ASCII
y, err := strconv.Atoi("123")
- 常量生成器
iota
创建一系列相关值,而不是逐个值显式写出,
iota
从0开始取值,逐项加1
type Weekday int
// sunday为0,以此类推
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
数组
默认情况下,一个新数组中的元素初始值为元素类型的零值
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // 0
不定长数组可用省略号来进行初始化
r := [...]int{1, 2, 3}
fmt.Println("%T\n", r) // "[3]int"
数组的元素类型如果可比较,数组大小也可以比较
package main
import "fmt"
func main() {
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b ==c) // true false false
d := [3]int{1, 2}
fmt.Println(a == d) // 编译错误
}
Go
把数组和其他的类型都看成值传递
当调用一个函数时,每个传入的参数都会创建一个副本,然后复赋值给对应的函数变量,所以函数接受的是一个副本,而不是原始的参数。
- 数组清零的方式
func zero(ptr *[32]byte) {
for i := range ptr {
ptr[i] = 0
}
}
func zero(ptr *[32]byte) {
*ptr = [32]byte{}
}
- 反转数组
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
slice
左移N元素(三次反转)
func main() {
a := []int{0, 1, 2, 3, 4, 5}
reverse(a[:2])
reverse(a[2:])
reverse(a)
fmt.Println(a) // 2 3 4 5 0 1
}
slice
slice
唯一允许比较的是nil
,想检查一个slice
是否为空,使用len(s) == 0
,而不是s == nil
,因为slice != nil
的情况下,也可能是空的
// 关于比较 不能用 == (slice是非直接的), 考虑 bytes.Equal
func equal(x, y []string) bool {
if len(x) != len(y) {
return false
}
for i := range(x) {
if x[i] != y[i] {
return false
}
}
return true
}
map
// map的比较
func euqal(x, y map[string]int) bool {
if len(x) != len(y) {
return false
}
for k, xv := range(x) {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
return true
}
struct
- 要求按照正确的顺序
- 变量名首字母大写可导出,否则为不可导出
- 允许定义不带名称的结构体,即匿名成员
- 支持结构体嵌套,直接用点的方式访问变量
- 函数
实参是按值传递的,所以函数接收到的是每个实参的副本。如果提供的实参包含引用类型,比如指针、slice
、map
、函数或者通道,那么当函数使用形参变量时就有可能会间接地修改实参变量。
- 迭代变量捕获
循环变量的作用域有规则限制,一般可以通过引用一个内部变量来解决
var rmdirs []func()
for _, d := range tempDirs() {
dir := d // 引用内部变量
os.MkdirAll(dir, 0755) // 创建父目录
rmdirs = append(rmdirs, func(){
os.RemoveAll(dir)
})
}
for _ rmdir := range rmdirs {
rmdir() // 清理
}
defer
defer
语句没有限制使用次数,执行的时候以调用defer
语句顺序的倒序进行,常用于打开关闭,连接断开,加锁解锁
package ioutil
// 关闭一个打开的文件
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
return ReadAll(f)
}
// 解锁一个互斥锁
var mu sync.Mutex
var m = make(map[string]int)
func looup(key string) int {
mu.Lock()
defer mu.Unlock()
return m[key]
}
延迟执行的匿名函数能够改变外层函数返回给调用者的结果
package main
import "fmt"
func main() {
var a int
a = triple(4)
fmt.Println(a) // 12
}
func triple(x int) (result int) {
defer func() { result += x}()
return double(x)
}
func double(x int) int {
return x + x
}
循环里的
defer
语句使用,一般将循环体以及defer
语句放到另一个函数里
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // 可能会用尽文件描述符
}
// 解决办法
for _, filename := range filenames {
if err := doFile(filename); err != nil {
return err
}
}
func doFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
}
- 关于宕机
一个典型的宕机发生时,正常的程序执行会终止,goroutine
中的所有延迟函数会执行,然后程序会异常退出并留下一条日志信息。日志消息包括宕机的值,这往往代表某种错误消息,每一个gorouting
都会在宕机的时候显示一个函数调用的栈跟踪信息
recover
用panic
和recover
写一个函数,没有return
,返回非零的值。
1.导致panic
异常的函数不会继续运行,但能正常返回
2.延迟执行的匿名函数能够改变外层函数返回给调用者的结果
package main
import "fmt"
func main() {
a := returnN()
fmt.Println(a) // 3
}
func returnN() (result int) {
defer func() {
if p := recover(); p != nil {
result = p.(int)
}
}()
panic(3)
}
time
go
中的时间格式化比较特别,需要记住一个特别的日期
Mon Jan 2 15:04:05 MST 2006
01/02 03:04:05PM ‘06 -0700 # 原因,好记
2006-01-02 15:04:05
所以格式化时间的写法有所改变
const (
layoutISO = "2006-01-02"
layoutUS = "January 2, 2006"
)
date := "1999-12-31"
t, _ := time.Parse(layoutISO, date)
fmt.Println(t) // 1999-12-31 00:00:00 +0000 UTC
fmt.Println(t.Format(layoutUS)) // December 31, 1999
error
满足error
接口的是*errorString
指针,而不是原始的errorString
,主要是为了让每次New
分配的error
实例都互不相等。
fmt.Println(errors.New("EOF") == errors.New("EOF")) // false
goroutine
Go
有两种并发编程的风格,goroutine
和channel
,它们支持通信顺序进程(Communicating Sequential Process, CSP
),CSP
是一个并发的模式,在不同的执行体(goroutine
)之间传递值,但是变量本身局限于单一的执行体
package main
import (
"fmt"
"time"
)
func main() {
go spinner(100 * time.Millisecond)
const n = 45
fibN := fib(n)
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}
// 等待动画
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
// 斐波那契数列
func fib(x int) int {
if x < 2{
return x
}
return fib(x-1) + fib(x-2)
}
- 可增长的栈
一个goroutine
在生命周期开始时只是一个很小的栈,典型情况下为2KB,但是与OS
不同的是栈的大小并不固定,它可以按需增大和缩小
- 调度
os
线程由os
内核来调度。每隔几毫秒,一个硬件时钟中断器发到CPU
,CPU
调用一个叫调度器的内核函数。这个函数暂停当前正在运行的线程,把它的寄存器信息保存到内存,查看线程列表并决定接下来运行哪一个线程,再从内存中恢复线程的注册表信息,最后执行选中的线程。这个操作考虑到内存的局域性以及设计的内存访问数量其实是比较慢的。
Go
运行时包含一个自己的调度器,称为m:n调度
(可以复用/调度m个goroutine
到n个OS
线程),Go
调度器只需关心单个Go
程序的goroutine
调度问题,调度器由特定的Go
语言结构来触发。如time.Sleep
或被通道阻塞或对互斥量操作时,调度器会将这个goroutine
设为休眠模式,并运行其他goroutine
直到前一个可重新唤醒为止。
调度成本低(不需要切换内核环境)
GOMAXPROCS
用来确定需要使用多少个OS
线程来同时执行Go
代码的参数,默认值是机器上的CPU
数量
goroutine
没有标识
设计如此,线程局部存储有一种被滥用的倾向
channel
通道是一种允许goroutine
之间传递特定值的一种通信机制,每一个通道是一个具体类型的导管,叫作通道的元素类型。主要操作为发送(send
)和接收(receive
),以及关闭(Close
)
ch := make(chan int) // 是make创建的数据结构的引用
ch <- x // 发送语句
x = <-ch // 赋值语句的接收表达式
<-ch // 接收语句,丢弃结果
- 无缓冲通道(强同步)
使用简单的make
调用创建的通道, make
还可以接受第二个参数从而生成有缓冲通道
ch := make(chan int) // 无缓冲通道
ch := make(chan int, 0) // 无缓冲通道
ch := make(chan int, 3) // 容量为3的缓冲通道
- 特性
无缓冲通道又称同步通道,无缓冲通道上的发送操作将会阻塞,直到另一个goroutine
在对应的通道上执行接受操作,这是值传送完成,两个goroutine
都可以继续执行。相反同理
- 关于关闭通道
结束时,不需要关闭每一个通道。只有在通知接收方goroutine
所有的数据都发送完毕的时候才需要关闭通道。通道也是可以通过垃圾回收器根据它是否可以访问来决定是否回收它,而不是根据它是否关闭。
package main
import "fmt"
func main() {
naturals := make(chan int)
squares := make(chan int)
// naturals
go func() {
for i := 0; i < 101; i++ {
naturals <- i
}
close(naturals)
}()
// squarer
go func() {
for i := range naturals {
squares <- i * i
}
close(squares)
}()
for i := range squares {
fmt.Println(i)
}
}
- 隐式转换
在任何赋值操作中将双向通道转换为单向通道都是允许的,但是反过来是不行的
- 缓冲通道(解耦)
缓冲通道有一个元素队列,有最大容量,发送操作在队列尾部插入,发送操作在队列头部移出,也存在同理的通道阻塞,即通道满阻塞发送,通道空阻塞接收
select
关于多路复用,也就是select
的case
语句,select
会一直等待,直到一次通信来告知一些情况可以执行。然后,进行此次通信,执行对应语句,其他通信将不会发生。如果没有对应情况,那么将永远等待
package main
import "fmt"
/* 缓冲区大小为1的通道,要么空,要么满,因此只有一个状况下可以运行,要么
i是偶数时发送,要么i是奇数时接收*/
func main() {
ch := make(chan int, 1) // 如果缓冲区大小变更,那么输出不稳定,select会抛硬币执行
for i := 0; i < 10; i ++ {
select {
case x := <-ch: // 奇数时接收
fmt.Println(x) // 0,2,4,6,8
case ch <- i: // 偶数时发送
}
}
}
- 关于break
标签化的break
语句将跳出select
和for
循环的逻辑; 没有标签的break
只能跳出select
的逻辑,导致循环的下一次迭代
- 关于cancel
加入取消机制可以通过创建一个取消通道,该通道不发送任何值,但是它的关闭状态可以用来需要停止它正在做的事情
var done = make(chan struct{})
// 定义一个工具函数,被调用的时候检测或者取消轮询状态
func cancelled() bool {
select {
case <-done:
return true
default:
return false
}
}
- 测试技巧
如果在取消事件到来的时候main
函数没有返回,执行一个panic
调用,然后运行时将转储程序中所有goroutine
的栈。如果主goroutine
是最后一个剩下的goroutine
,它需要自己进行清理。但如果还有其他的goroutine
存活,它们可能还没有合适地取消,或者它们已经取消
竞态
- 定义
多个goroutine
按某些交错顺序执行时程序无法给出正确的结果
- 解决方案
1.不修改变量,包初始化后就不可再修改
2.避免从多个goroutine
中访问同一个变量(如果要访问,那么应该通过通道)
*Go箴言: 不要通过共享内存来通信,而应该通过通信来共享内存
3.允许多个goroutine
访问同一个变量,但是在同一时间只有一个goroutine
可以访问,即互斥
- 竞态检测器
报告所有实际运行了的数据竞态,注意只检测在运行时发生的竞态,无法用来保证肯定不会发生竞态
# 命令行参数附加 -race
go bulid/run/test mian.go -race
sync
- 互斥锁
var mu sync.Mutex
func f(...)... {
mu.Lock()
defer mu.Unlock()
return ...
}
- 读写互斥锁(多读单写)
var mu sync.RWMutex
func f(...)... {
mu.RLock()
defer mu.RUnlock()
return ...
}
*注意:一般来讲,不应该假定那些逻辑上只读的函数和方法不会更新一些变量。比如,一个看起来只是简单访问器的方法可能会递增内部使用的计数器,或者更新一个缓存来让重复的调用更快。
反射
- 定义
在编译时不知道类型的情况下,可更新变量、在运行时查看值、调用方法以及直接对它们的布局进行操作
- 为何会用到反射
// 模拟格式化输出
func Sprintf(x interface{}) string {
type stringer interface {
String() string
}
switch x := x.(type) {
case stringer:
return x.String()
case string:
return x
case int:
return strconv.Itoa(x)
case bool:
if x{
return "true"
}
return "false"
default:
// array, chan, func, map, pointer, slice, struct
return "???"
}
}
- 隐式转换
把一个具体值赋给一个接口类型时会发生一个隐式类型转换,转换生成一个包含两部分内容的接口值:动态类型
部分是操作数的类型(int), 动态值
部分是操作数的值
// reflect.TypeOf
t := reflect.TypeOf(3)
fmt.Println(t.String()) // int
fmt.Println(t) // int
// reflect.ValueOf
v := reflect.ValueOf(3)
fmt.Println(v) // "3"
fmt.Printf("%v\n", v) // "3"
fmt.Println(v.String()) // "<int Value>"
- 聚合类型的反射
主要思路为递归
,多个case
分支里逐一对slice
,map
, ptr
,struct
等进行分析递归
reflect.value
设置值
// CanAddr
reflect.Println(x.CanAddr()) // 返回Bool值
思路:调用Addr()
,返回一个Value
,其中包含一个指向变量的指针,接下来,在这个Value
上调用Interface()
,会返回一个包含这个指针的interface{}
的值。最后,我们知道变量的类型,就可以使用类型断言来把接口内容转换为一个普通指针,然后通过指针修改变量。
x := 2
d := reflect.ValueOf(&x).Elem() // d为变量
px := d.Addr().Interface().(*int) // px := &x
*px = 3 // x = 3
fmt.Println(x) // "3"
reflect.Value.Set
// 可直接调用set方法进行修改
d.Set(reflect.ValueOf(4)) // 不可寻址用set会崩溃
CanSet
检验一个变量是否可以正确更新(考虑到反射可以读取到未导出结构字段的值)
fmt.Println(fd.CanAddr(), fd.CanSet()) // 返回bool值
- 注意反射的三个弱点
1.基于反射的代码很脆弱
2.类型其实也算是某种形式的文档,无法静态类型检查
3.慢(比起特定类型优化函数)
Go-gin
gin
go get -u github.com/gin-gonic/gin # 拉取gin的依赖
go env
go env -w GOPROXY=https://goproxy.cn,direct # 代理设置 否则拉取不到
go env -w GO111MODULE=on
go mod
go mod init [module] # module 为项目名称 module是一个相关Go包的集合,源代码更替和控制的单元 用来取代GOPATH, 有点像python的requirement.txt
Engine
创建引擎分为两种方式,分别为
gin.New()
和gin.Default()
gin.Default
不同于gin.New(),它拥有Logger()
和Recovery()
两种中间件
Logger
: 负责进行打印并输出日志的中间件,方便开发者进行程序调试;
Recovery
: 如果程序执行过程中遇到panc中断了服务, 恢复程序执行,并返回500
(服务器错误)
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
tidy
go mod tidy # 安裝依賴