LJKのBlog

学无止境

zap日志级别

zap日志级别,一共7级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const (
// DebugLevel logs are typically voluminous, and are usually disabled in production.
DebugLevel Level = iota - 1
// InfoLevel is the default logging priority.
InfoLevel
// WarnLevel logs are more important than Info, but don't need individual human review.
WarnLevel
// ErrorLevel logs are high-priority. If an application is running smoothly, it shouldn't generate any error-level logs.
ErrorLevel
// DPanicLevel logs are particularly important errors. In development the logger panics after writing the message.
DPanicLevel
// PanicLevel logs a message, then panics.
PanicLevel
// FatalLevel logs a message, then calls os.Exit(1).
FatalLevel

_minLevel = DebugLevel
_maxLevel = FatalLevel
)

zap官方案例

zap 的官方案例中,介绍了三种使用方式:

  • AdvancedConfiguration:高级配置
  • BasicConfiguration:基本配置
  • Presets:预置

Presets

Presets(预置函数) 是 zap 日志包最简单的用法,不需要过多的自定义配置就可以用起来。

Presets 官网示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import (
"time"

"go.uber.org/zap"
)

func main() {
logger := zap.NewExample() // or NewProduction, or NewDevelopment
defer logger.Sync()

const url = "http://example.com"

sugar := logger.Sugar()
sugar.Infow("Failed to fetch URL.",
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)

logger.Info("Failed to fetch URL.",
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
}

BasicConfiguration

当预置函数的logger配置不满足我们的需求时,可以自定义配置logger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"encoding/json"

"go.uber.org/zap"
)

func main() {
// For some users, the presets offered by the NewProduction, NewDevelopment,
// and NewExample constructors won't be appropriate. For most of those
// users, the bundled Config struct offers the right balance of flexibility
// and convenience. (For more complex needs, see the AdvancedConfiguration
// example.)
//
// See the documentation for Config and zapcore.EncoderConfig for all the
// available options.
rawJSON := []byte(`{
"level": "debug",
"encoding": "json",
"outputPaths": ["stdout", "/tmp/logs"],
"errorOutputPaths": ["stderr"],
"initialFields": {"foo": "bar"},
"encoderConfig": {
"messageKey": "message",
"levelKey": "level",
"levelEncoder": "lowercase"
}
}`)

var cfg zap.Config
if err := json.Unmarshal(rawJSON, &cfg); err != nil {
panic(err)
}
logger, err := cfg.Build()
if err != nil {
panic(err)
}
defer logger.Sync()

logger.Info("logger construction succeeded")
}

AdvancedConfiguration

如果需要更多的功能,例如:分隔文件、将日志发送到消息队列等,需要引入 go.uber.org/zap/zapcore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"io/ioutil"
"os"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func main() {
// The bundled Config struct only supports the most common configuration
// options. More complex needs, like splitting logs between multiple files
// or writing to non-file outputs, require use of the zapcore package.
//
// In this example, imagine we're both sending our logs to Kafka and writing
// them to the console. We'd like to encode the console output and the Kafka
// topics differently, and we'd also like special treatment for
// high-priority logs.

// First, define our level-handling logic.
highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl >= zapcore.ErrorLevel
})
lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl < zapcore.ErrorLevel
})

// Assume that we have clients for two Kafka topics. The clients implement
// zapcore.WriteSyncer and are safe for concurrent use. (If they only
// implement io.Writer, we can use zapcore.AddSync to add a no-op Sync
// method. If they're not safe for concurrent use, we can add a protecting
// mutex with zapcore.Lock.)
topicDebugging := zapcore.AddSync(ioutil.Discard)
topicErrors := zapcore.AddSync(ioutil.Discard)

// High-priority output should also go to standard error, and low-priority
// output should also go to standard out.
consoleDebugging := zapcore.Lock(os.Stdout)
consoleErrors := zapcore.Lock(os.Stderr)

// Optimize the Kafka output for machine consumption and the console output
// for human operators.
kafkaEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())

// Join the outputs, encoders, and level-handling functions into
// zapcore.Cores, then tee the four cores together.
core := zapcore.NewTee(
zapcore.NewCore(kafkaEncoder, topicErrors, highPriority),
zapcore.NewCore(consoleEncoder, consoleErrors, highPriority),
zapcore.NewCore(kafkaEncoder, topicDebugging, lowPriority),
zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority),
)

// From a zapcore.Core, it's easy to construct a Logger.
logger := zap.New(core)
defer logger.Sync()
logger.Info("constructed a logger")
}

zap&zapcore知识点详解

生产配置参考

Context 包定义了上下文类型,该上下文类型跨越 API 边界和进程之间传递截止期限,取消信号和其他请求范围值。

对服务器的传入请求应创建一个 Context,对服务器的传出调用应接受 Context。它们之间的函数调用链必须传播 Context,可以用使用 WithCancel,WithDeadline,WithTimeout 或 WithValue 创建的派生上下文替换。当 Context 被取消时,从它派生的所有 Context 也被取消。

WithCancel,WithDeadline 和 WithTimeout 函数采用 Context(父级)并返回派生的 Context(子级)和 CancelFunc。调用 CancelFunc 将取消子对象及其子对象,删除父对子对象的引用,并停止任何关联的定时器。未能调用 CancelFunc 会泄漏子项及其子项,直至父项被取消或计时器激发。go vet 工具检查在所有控制流路径上使用 CancelFuncs。

使用 Contexts 的程序应该遵循这些规则来保持包之间的接口一致,并使静态分析工具能够检查上下文传播:

不要将上下文存储在结构类型中;相反,将一个 Context 明确地传递给每个需要它的函数。上下文应该是第一个参数,通常命名为 ctx:

1
2
3
func DoSomething(ctx context.Context, arg Arg) error {
// ... use ctx ...
}

即使函数允许,也不要传递 nil Context。如果您不确定要使用哪个 Context,请传递 context.TODO。

使用上下文值仅适用于传输进程和 API 的请求范围数据,而不用于将可选参数传递给函数。

相同的上下文可以传递给在不同 goroutine 中运行的函数; 上下文对于多个 goroutine 同时使用是安全的。

有关使用 Contexts 的服务器的示例代码,请参阅https://blog.golang.org/context。

Variables

Canceled

1
var Canceled = errors.New("context canceled")

Canceled is the error returned by Context.Err when the context is canceled.

DeadlineExceeded

1
var DeadlineExceeded error = deadlineExceededError{}

DeadlineExceeded is the error returned by Context.Err when the context’s deadline passes. 截止时间过后返回的错误

type CancelFunc

1
type CancelFunc func()

func WithCancel

1
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel函数用来创建一个可取消的context,返回一个context和一个CancelFunc,调用CancelFunc即可触发cancel操作。

取消 Context 会释放与它相关的资源,所以只要完成在 Context 中运行的操作,代码就应该调用 cancel。

示例:

这个例子演示了如何使用可取消的 context 来防止 goroutine 泄漏。在示例函数结束时,由 gen 启动的 goroutine 将返回而不会泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"context"
"fmt"
)

func main() {
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // returning not to leak the goroutine 返回不泄漏goroutine
case dst <- n:
n++
}
}
}()
return dst
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers

for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}

// output:
// 1
// 2
// 3
// 4
// 5

func WithDeadline

1
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

WithDeadline返回一个基于parent的可取消的context,并且其过期时间deadline不晚于所设置时间d

如果父节点parent有过期时间并且过期时间早于给定时间d,那么新建的子节点context无需设置过期时间,使用WithCancel创建一个可取消的context即可;

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)

// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()

select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}

func WithTimeout

WithDeadline类似,WithTimeout也是创建一个定时取消的context,只不过WithDeadline是接收一个过期时间点,而WithTimeout接收一个相对当前时间的过期时长timeout:

1
2
3
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}

type Context

context 包含截止日期取消信号以及跨越 API 边界的其他值

上下文的方法可能会被多个 goroutine 同时调用。

1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
  • Deadline返回绑定当前context的任务被取消的截止时间;如果没有设定期限,将返回ok == false
  • Done 当绑定当前context的任务被取消时,将返回一个关闭的channel;如果当前context不会被取消,将返回nil
  • Err 如果Done返回的channel没有关闭,将返回nil;如果Done返回的channel已经关闭,将返回非空的值表示任务结束的原因。如果是context被取消,Err将返回Canceled;如果是context超时,Err将返回DeadlineExceeded
  • Value 返回context存储的键值对中当前key对应的值,如果没有对应的key,则返回nil

func Background

emptyCtx是一个int类型的变量,但实现了context的接口。emptyCtx没有超时时间,不能取消,也不能存储任何额外信息,所以emptyCtx用来作为context树的根节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}

func (*emptyCtx) Done() <-chan struct{} {
return nil
}

func (*emptyCtx) Err() error {
return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}

func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}

var (
background = new(emptyCtx)
todo = new(emptyCtx)
)

func Background() Context {
return background
}

func TODO() Context {
return todo
}

但我们一般不会直接使用emptyCtx,而是使用由emptyCtx实例化的两个变量,分别可以通过调用BackgroundTODO方法得到,这两个context在实现上是一样的。

func TODO

BackgroundTODO 方法在某种层面上看其实也只是互为别名,两者没有太大的差别,不过 context.Background() 是上下文中最顶层的默认值,所有其他的上下文都应该从 context.Background() 演化出来。

1646128337346

我们应该只在不确定时使用 context.TODO(),在多数情况下如果函数没有上下文作为入参,我们往往都会使用 context.Background() 作为起始的 Context 向下传递。

func WithValue

WithValue用以向context添加键值对:

1
2
3
4
5
6
7
8
9
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}

这里添加键值对不是在原context结构体上直接添加,而是以此context作为父节点,重新创建一个新的valueCtx子节点,将键值对添加在子节点上,由此形成一条context链。获取value的过程就是在这条context链上由尾部上前搜寻。

返回parent的一个副本,调用该副本的 Value(key)方法将得到 val。这样我们不光将根节点原有的值保留了,还在子孙节点中加入了新的值,注意若存在 Key 相同,则会被覆盖。

注意:为了避免使用 context 的包之间发生冲突,key 必须具有可比性( comparable ),不应该是 string 或者其他任何内置类型。应该使用自定义类型。

总结

Go 语言中的 Context 的主要作用还是在多个 Goroutine 或者模块之间同步取消信号或者截止日期,用于减少对资源的消耗和长时间占用,避免资源浪费,虽然传值也是它的功能之一,但是这个功能我们还是很少用到。

在真正使用传值的功能时我们也应该非常谨慎,不能将请求的所有参数都使用 Context 进行传递,这是一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。

context包通过构建树型关系的 Context,来达到上一层 Goroutine 能对传递给下一层 Goroutine 的控制。对于处理一个 Request 请求操作,需要采用context来层层控制 Goroutine,以及传递一些变量来共享。

  • Context 对象的生存周期一般仅为一个请求的处理周期。即针对一个请求创建一个 Context 变量(它为 Context 树结构的根);在请求处理结束后,撤销此 ctx 变量,释放资源。
  • 每次创建一个 Goroutine,要么将原有的 Context 传递给 Goroutine,要么创建一个子 Context 并传递给 Goroutine。
  • Context 能灵活地存储不同类型、不同数目的值,并且使多个 Goroutine 安全地读写其中的值。
  • 当通过父 Context 对象创建子 Context 对象时,可同时获得子 Context 的一个撤销函数,这样父 Context 对象的创建环境就获得了对子 Context 将要被传递到的 Goroutine 的撤销权。

使用原则

  • 不要把 Context 存在一个结构体当中,显式地传入函数。Context 变量需要作为第一个参数使用,一般命名为 ctx;
  • 即使方法允许,也不要传入一个 nil 的 Context,如果你不确定你要用什么 Context 的时候传一个 context.TODO;
  • 使用 context 的 Value 相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数;
  • 同样的 Context 可以用来传递到不同的 goroutine 中,Context 在多个 goroutine 中是安全的;
  • 在子 Context 被传递到的 goroutine 中,应该对该子 Context 的 Done 信道(channel)进行监控,一旦该信道被关闭(即上层运行环境撤销了本 goroutine 的执行),应主动终止对当前请求信息的处理,释放资源并返回。

通常,context.Background()作为最顶层的 Context,在此基础上创建可撤销的 Context。

读锁:读读不互斥。加了读锁,其他协程仍然可以读,但是不能写
写锁:独占。加了写锁,其他协程无论读写,都不被允许

sync包我们一般不用,推荐使用channel,但是我们也要熟悉。

互斥锁和自旋锁

互斥锁

共享资源的使用是互斥的,即一个线程获得资源的使用权后就会将该资源加锁,使用完后会将其解锁,如果在使用过程中有其他线程想要获取该资源的锁,那么它就会被阻塞陷入睡眠状态,直到该资源被解锁才会被唤醒,如果被阻塞的资源不止一个,那么它们都会被唤醒,但是获得资源使用权的是第一个被唤醒的线程,其它线程又陷入沉睡。

互斥锁在各个语言中定义都不太相同,在大多数语言中,互斥锁使用线程调度来实现的,假如现在锁被锁住了,那么后面的线程就会进入”休眠”状态,直到解锁之后,又会唤醒线程继续执行。这也叫空等待(sleep-waiting)。

严格来说,只要是同一时刻只能被拿到一次的锁都叫互斥锁,自旋锁也是

在golang中, sync.Mutex就是一个开箱即用的互斥锁,它能保证在同一时刻只有一个”协程”能拿到锁,golang中就同时使用了”自旋”和”休眠”两种方式来实现互斥锁。

自旋锁

自旋锁也是广义上的互斥锁,是互斥锁的实现方式之一,它不会产生线程的调度,而是通过”循环”来尝试获取锁,优点是能很快的获取锁,缺点是会占用过多的CPU时间,这被称为忙等待(busy-waiting)。

interface Locker

1
2
3
4
type Locker interface {
Lock()
Unlock()
}

Locker接口代表一个可以加锁和解锁的对象。

type Once

1
2
3
4
type Once struct {
done uint32
m Mutex
}

Once是只执行一次动作的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
"fmt"
"sync"
)

func main() {
var once sync.Once
onceBody := func() {
fmt.Println("Only once")
}
for i := 0; i < 10; i++ {
once.Do(onceBody)
}
}

// Only once

func (o *Once) Do(f func())

1
func (o *Once) Do(f func())

Do方法当且仅当第一次被调用时才执行函数f。换句话说,给定变量: var once sync.Once, 如果once.Do(f)被多次调用,只有第一次调用会执行f,即使f每次调用Do提供的f值不同。

Do用于必须刚好运行一次的初始化。因为f是没有参数的,因此可能需要使用闭包来提供给Do方法调用:

1
config.once.Do(func() { config.init(filename) })

因为只有f返回后Do方法才会返回,f若引起了Do的调用,会导致死锁。

type Mutex

1
2
3
4
type Mutex struct {
state int32
sema uint32
}

Mutex是一个互斥锁,又叫拍他锁或者写锁,零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的goroutine加锁和解锁。

如果想要区分读、写锁,可以使用sync.RWMutex类型,见后文。

在Lock()和Unlock()之间的代码段称为资源的临界区(critical section),在这一区间内的代码是严格被Lock()保护的,是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码

func (m *Mutex) Lock()

1
func (m *Mutex) Lock()

声明互斥锁m:var m sync.Mutex

m.Lock() 对m加互斥锁,如果再次执行m.Lock(),则会阻塞,直到m.Unlock()对m解锁,第二次对m加锁才会生效。以此实现对数据的保护。

func (m *Mutex) Unlock()

1
func (m *Mutex) Unlock()

声明互斥锁m:var m sync.Mutex

m.Unlock()对m解锁,如果m未加锁会导致运行时错误:panic: sync: unlock of unlocked mutex

type RWMutex

读锁:又叫共享锁,可以并发读

1
2
3
4
5
6
7
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // 写锁需要等待读锁释放的信号量
readerSem uint32 // 读锁需要等待写锁释放的信号量
readerCount int32 // 读锁后面挂起了多少个写锁申请
readerWait int32 // 已释放了多少个读锁
}

RWMutex是读写锁。 该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景;零值为解锁状态。RWMutex类型的锁也和线程无关,可以由不同的goroutine加读取锁/写入和解读取锁/写入锁。

RWMutex是基于Mutex的,在Mutex的基础之上增加了读、写的信号量,并使用了类似引用计数的读锁数量。

声明一个读写锁:var rw sync.RWMutex

  • rw 可以同时加多个读锁
  • rw 加读锁时再加写锁将阻塞,有写锁时再加读锁将阻塞
  • rw 加写锁,后续再加读锁和写锁都将阻塞

func (rw *RWMutex) Lock()

1
func (rw *RWMutex) Lock()

对 rw 加写锁,后续再加读锁和写锁都将阻塞

func (rw *RWMutex) Unlock()

1
func (rw *RWMutex) Unlock()

对 rw 解写锁

func (rw *RWMutex) RLock()

1
func (rw *RWMutex) RLock()

对 rw 加读锁,在执行RUnlock解读锁之前,后续可以再加读锁,但是加写锁会阻塞

func (rw *RWMutex) RUnlock()

1
func (rw *RWMutex) RUnlock()

对 rw 解读锁

func (rw *RWMutex) RLocker() Locker

返回一个实现了Lock()和Unlock()方法的Locker接口

type Cond

1
2
3
4
5
type Cond struct {
// 在观测或更改条件时L会冻结
L Locker
// 包含隐藏或非导出字段
}

参考:

https://studygolang.com/articles/28072
https://segmentfault.com/a/1190000038319116
https://zhuanlan.zhihu.com/p/351776260

func NewCond(l Locker)

func (c *Cond) Broadcast()

func (c *Cond) Signal()

func (c *Cond) Wait()

type Map

Go中的map不是并发安全的,在Go1.9之后,引入了sync.Map:并发安全的map

参考:

https://www.cnblogs.com/ricklz/p/13659397.html
http://c.biancheng.net/view/34.html

func (m *Map) Delete(key interface{})

func (m *Map) Load(key interface{}) (value interface{}, ok bool)

func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)

func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)

func (m *Map) Range(f func(key, value interface{}) bool)

func (m *Map) Store(key, value interface{})

type Pool

pool:资源池

参考:https://www.jianshu.com/p/8fbbf6c012b2

  • 临时对象
  • 自动移除
  • 当这个对象的引用只有sync.Pool持有时,这个对象内存会被释放
  • 多线程安全
  • 目的就是缓存并重用对象,减少GC的压力
  • 自动扩容、缩容
  • 不要去拷贝pool,也就是说最好单例

Pool的合理用法是用于管理一组静静的被多个独立并发线程共享并可能重用的临时item。

func (p *Pool) Get

1
func (p *Pool) Get() interface{}

Get方法从池中选择任意一个item,删除其在池中的引用计数,并提供给调用者。Get方法也可能选择无视内存池,将其当作空的。调用者不应认为Get的返回这和传递给Put的值之间有任何关系。

假使Get方法没有取得item:如p.New非nil,Get返回调用p.New的结果;否则返回nil。

func (p *Pool) Put

1
func (p *Pool) Put(x interface{})

Put方法将x放入池中。

type WaitGroup

1
2
3
4
type WaitGroup struct {
noCopy noCopy
state1 [3]uint32
}

WaitGroup用于等待一组goroutine的结束。父goroutine调用Add方法来设定应等待的goroutine的数量。每个被等待的goroutine在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。

示例:

1
2
3
4
5
6
7
8
9
10
11
func main() {
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}

注意:

-

func (wg *WaitGroup) Add

1
func (wg *WaitGroup) Add(delta int)

Add方法向内部计数加上delta,delta可以是负数;如果内部计数器变为0,Wait方法阻塞等待的所有goroutine都会释放,如果计数器小于0,方法panic。注意Add调用应在Wait之前,否则Wait可能只会等待很少的goroutine。一般来说本方法应在创建新的goroutine或者其他应等待的事件之前调用。

func (wg *WaitGroup) Done

1
2
3
func (wg *WaitGroup) Done() {
wg.Add(-1)
}

Done方法减少WaitGroup计数器的值,应在每个等待的goroutine的最后执行。

func (wg *WaitGroup) Wait

1
func (wg *WaitGroup) Wait()

Wait方法阻塞直到WaitGroup计数器减为0。

Variables

ErrConnDone

在已经提交或回滚的连接上执行的任何操作都会返回 ErrConnDone。

1
var ErrConnDone = errors.New("database/sql: connection is already closed")

ErrNoRows

当 QueryRow 没有返回行时,Scan 返回 ErrNoRows。在这种情况下,QueryRow 会返回一个占位符* Row 值,该值将推迟发生此错误直到扫描。

1
var ErrNoRows = errors.New("sql: no rows in result set")

ErrTxDone

对已经提交或回滚的事务执行的任何操作都会返回 ErrTxDone。

1
var ErrTxDone = errors.New("sql: Transaction has already been committed or rolled back")

func Drivers() []string

1
func Drivers() []string

驱动程序返回已注册驱动程序名称的排序列表。

func Register(name string, driver driver.Driver)

1
func Register(name string, driver driver.Driver)

Register 使用提供的名称提供数据库驱动程序。如果使用相同的名称调用 Register 两次,或者驱动程序为零,则会发生混乱。

type ColumnType

1
func (rs *Rows) ColumnTypes() ([]*ColumnType, error)
1
2
3
type ColumnType struct {
// 包含过滤或未导出的字段
}

ColumnType 包含列的名称和类型。

func (*ColumnType) DatabaseTypeName

1
func (ci *ColumnType) DatabaseTypeName() string

DatabaseTypeName 返回列类型的数据库系统名称。如果返回空字符串,则不支持驱动程序类型名称。请查阅您的驱动程序文档以获取驱动程序数据类型列表 不包括长度说明符。常见类型包括 “VARCHAR”,“TEXT”,“NVARCHAR”,“DECIMAL”,“BOOL”,“INT”,“BIGINT”。

func (*ColumnType) DecimalSize

1
func (ci *ColumnType) DecimalSize() (precision, scale int64, ok bool)

DecimalSize 返回小数类型的比例和精度。如果不适用或者不支持,那么ok 是错误的。

func (*ColumnType) Length

1
func (ci *ColumnType) Length() (length int64, ok bool)

Length 返回可变长度列类型(如文本和二进制字段类型)的列类型长度。如果类型长度没有限制,则数值为 math.MaxInt64(任何数据库限制仍将适用)。如果列类型不是可变长度,例如 int,或者如果驱动程序不支持,那么 ok 是 false。

func (*ColumnType) Name

1
func (ci *ColumnType) Name() string

Name 返回列的名称或别名。

func (*ColumnType) Nullable

1
func (ci *ColumnType) Nullable() (nullable, ok bool)

Nullable 返回列是否可以为 null。如果一个驱动程序不支持这个属性,ok 将会是 false。

func (*ColumnType) ScanType

1
func (ci *ColumnType) ScanType() reflect.Type

ScanType 使用 Rows.Scan 返回适合扫描的 Go 类型。如果驱动程序不支持此属性,ScanType 将返回空接口的类型。

type Conn

1
func (db *DB) Conn(ctx context.Context) (*Conn, error)
1
2
3
type Conn struct {        
// 包含过滤或未导出的字段
}

Conn 代表一个数据库会话而不是一个数据库会话池。除非特别需要连续的单个数据库会话,否则首选运行 DB 查询。

Conn 必须调用 Close 来将连接返回到数据库池,并且可以与正在运行的查询同时进行。

在调用 Close 之后,连接上的所有操作都会因 ErrConnDone 而失败。

func (*Conn) BeginTx

1
func (c *Conn) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)

BeginTx 开始一个事务。

提供的上下文用于事务提交或回滚之前。如果上下文被取消,sql包将回滚事务。如果提供给 BeginTx 的上下文被取消,则Tx.Commit 将返回错误。

提供的 TxOptions 是可选的,如果应该使用默认值,则可能为零。如果使用驱动程序不支持的非默认隔离级别,则会返回错误。

func (*Conn) Close

1
func (c *Conn) Close() error

Close 将连接返回到连接池。Close 后的所有操作都将返回ErrConnDone。Close 是安全地与其他操作同时进行,并会阻止,直到所有其他操作完成。首先取消使用的上下文,然后直接调用 close 可能会很有用。

func (*Conn) ExecContext

1
func (c *Conn) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error)

ExecContext 执行查询而不返回任何行。这些参数用于查询中的任何占位符参数。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"context"
"database/sql"
"log"
)

var (
ctx context.Context
db *sql.DB
)

func main() {
// A *DB is a pool of connections. Call Conn to reserve a connection for
// exclusive use.
conn, err := db.Conn(ctx)
if err != nil {
log.Fatal(err)
}
defer conn.Close() // Return the connection to the pool.
id := 41
result, err := conn.ExecContext(ctx, `UPDATE balances SET balance = balance + 10 WHERE user_id = ?;`, id)
if err != nil {
log.Fatal(err)
}
rows, err := result.RowsAffected()
if err != nil {
log.Fatal(err)
}
if rows != 1 {
log.Fatalf("expected single row affected, got %d rows affected", rows)
}
}

func (*Conn) PingContext

1
func (c *Conn) PingContext(ctx context.Context) error

PingContext 验证到数据库的连接是否仍然存在。

func (*Conn) PrepareContext

1
func (c *Conn) PrepareContext(ctx context.Context, query string) (*Stmt, error)

PrepareContext 为以后的查询或执行创建一个准备好的语句。可以从返回的语句中同时运行多个查询或执行。当语句不再需要时,调用者必须调用语句的 Close 方法。

提供的上下文用于编写声明,而不用于执行声明。

func (*Conn) QueryContext

1
func (c *Conn) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)

QueryContext 执行一个返回行的查询,通常是一个 SELECT。该参数适用于查询中的任何占位符参数。

func (*Conn) QueryRowContext

1
func (c *Conn) QueryRowContext(ctx context.Context, query string, args ...interface{}) *Row

QueryRowContext 执行一个预计最多只返回一行的查询。QueryRowContext 总是返回一个非零值。错误被推迟到行的扫描方法被调用。如果查询未选择行,则*Row 的扫描将返回错误号。否则,*Row 的扫描将扫描第一个选定的行并丢弃其余行。

func (*Conn) Raw

1
func (c *Conn) Raw(f func(driverConn interface{}) error) (err error)

Raw executes f exposing the underlying driver connection for the duration of f. The driverConn must not be used outside of f.

Once f returns and err is nil, the Conn will continue to be usable until Conn.Close is called.

Raw执行f暴露底层驱动连接的持续时间。driverConn不能在f之外使用。一旦f返回并且err为nil, Conn将继续可用,直到Conn. close被调用。

type DB ★★★★★

1
2
3
type DB struct {
// contains filtered or unexported fields
}

DB是一个数据库句柄,表示一个包含零个或多个底层连接的池。多个 goroutine 并发使用是安全的。

sql 包自动创建并释放连接;它也保持空闲连接的空闲池。如果数据库具有每个连接状态的概念,则只能在事务内可靠地观察到这种状态。一旦 DB.Begin 被调用,返回的 Tx 就绑定到单个连接。在事务上调用 Commit 或 Rollback 后,该事务的连接将返回到数据库的空闲连接池。池的大小可以通过 SetMaxIdleConns 进行控制。

func Open

1
func Open(driverName, dataSourceName string) (*DB, error)

Open 打开一个由其数据库驱动程序名称和驱动程序特定的数据源名称指定的数据库,通常由至少一个数据库名称和连接信息组成。

大多数用户将通过驱动程序特定的连接帮助程序函数打开数据库,该函数返回一个 *DB。Go标准库中不包含数据库驱动程序。有关第三方驱动程序的列表,请参阅https://golang.org/s/sqldrivers。

Open 可能只是验证其参数而不创建与数据库的连接。要验证数据源名称是否有效,请调用 Ping。

返回的数据库对于多个 goroutine 的并发使用是安全的,并保持其自己的空闲连接池。因此,Open 函数应该只调用一次。很少有必要关闭数据库。

func (*DB) Begin

1
2
3
func (db *DB) Begin() (*Tx, error) {
return db.BeginTx(context.Background(), nil)
}

Begin 开始一个事务。默认隔离级别取决于驱动程序。

func (*DB) BeginTx

1
func (db *DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)

BeginTx 开始一个事务。

提供的上下文用于事务提交或回滚之前。如果上下文被取消,sql包将回滚事务。如果提供给 BeginTx 的上下文被取消,则Tx.Commit 将返回错误。

提供的 TxOptions 是可选的,如果应该使用默认值,则可能为零。如果使用驱动程序不支持的非默认隔离级别,则会返回错误。

func (*DB) Close

1
func (db *DB) Close() error

func (db *DB) Conn

1
func (db *DB) Conn(ctx context.Context) (*Conn, error)

返回单个连接。

func (db *DB) Driver

1
func (db *DB) Driver() driver.Driver

Driver 返回数据库的底层驱动程序。

func (db *DB) Exec

1
2
3
func (db *DB) Exec(query string, args ...interface{}) (Result, error) {
return db.ExecContext(context.Background(), query, args...)
}

Exec 执行写语句,不返回任何rows。该参数适用于查询中的任何占位符参数。

func (db *DB) ExecContext

1
func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error)

func (db *DB) Ping

1
2
3
func (db *DB) Ping() error {
return db.PingContext(context.Background())
}

Ping 验证与数据库的连接仍然存在,并在必要时建立连接。

func (db *DB) PingContext

1
func (db *DB) PingContext(ctx context.Context) error

func (db *DB) Prepare

1
2
3
func (db *DB) Prepare(query string) (*Stmt, error) {
return db.PrepareContext(context.Background(), query)
}

Prepare 方法实现预处理操作:先将sql语句发送给MySQL服务端,返回一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令。

读操作 预处理示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 预处理查询示例
func prepareQueryDemo() {
sqlStr := "select id, name, age from user where id > ?"
stmt, err := db.Prepare(sqlStr)
if err != nil {
fmt.Printf("prepare failed, err:%v\n", err)
return
}
defer stmt.Close()
rows, err := stmt.Query(0)
if err != nil {
fmt.Printf("query failed, err:%v\n", err)
return
}
defer rows.Close()
// 循环读取结果集中的数据
for rows.Next() {
var u user
err := rows.Scan(&u.id, &u.name, &u.age)
if err != nil {
fmt.Printf("scan failed, err:%v\n", err)
return
}
fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
}
}

写操作 预处理也时分类似,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 预处理插入示例
func prepareInsertDemo() {
sqlStr := "insert into user(name, age) values (?,?)"
stmt, err := db.Prepare(sqlStr)
if err != nil {
fmt.Printf("prepare failed, err:%v\n", err)
return
}
defer stmt.Close()
_, err = stmt.Exec("小王子", 18)
if err != nil {
fmt.Printf("insert failed, err:%v\n", err)
return
}
_, err = stmt.Exec("沙河娜扎", 18)
if err != nil {
fmt.Printf("insert failed, err:%v\n", err)
return
}
fmt.Println("insert success.")
}

func (db *DB) PrepareContext

1
func (db *DB) PrepareContext(ctx context.Context, query string) (*Stmt, error)

func (db *DB) Query

1
2
3
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
return db.QueryContext(context.Background(), query, args...)
}

多行查询:执行一次查询,返回多行结果(即Rows),一般用于执行select命令。参数args表示query中的占位参数。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 查询多条数据示例
func queryMultiRowDemo() {
sqlStr := "select id, name, age from user where id > ?"
rows, err := db.Query(sqlStr, 0)
if err != nil {
fmt.Printf("query failed, err:%v\n", err)
return
}
// 非常重要:关闭rows释放持有的数据库链接
defer rows.Close()

// 循环读取结果集中的数据
for rows.Next() {
var u user
err := rows.Scan(&u.id, &u.name, &u.age)
if err != nil {
fmt.Printf("scan failed, err:%v\n", err)
return
}
fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
}
}

func (db *DB) QueryContext

1
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)

func (db *DB) QueryRow

1
2
3
4
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *Row {
rows, err := db.QueryContext(ctx, query, args...)
return &Row{rows: rows, err: err}
}

单行查询:执行一次查询,并期望返回最多一行结果(即Row)。QueryRow总是返回非nil的值,直到返回值的Scan方法被调用时,才会返回被延迟的错误。(如:未找到结果)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
// 查询单条数据示例
func queryRowDemo() {
sqlStr := "select id, name, age from user where id=?"
var u user
// 非常重要:确保QueryRow之后调用Scan方法,否则持有的数据库链接不会被释放
err := db.QueryRow(sqlStr, 1).Scan(&u.id, &u.name, &u.age)
if err != nil {
fmt.Printf("scan failed, err:%v\n", err)
return
}
fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
}

func (db *DB) QueryRowContext

1
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *Row

func (db *DB) SetConnMaxIdleTime

1
func (db *DB) SetConnMaxIdleTime(d time.Duration)

设置连接空闲的最大时间。过期的连接在复用之前可能会被延迟关闭。如果d <= 0,则连接不会因为空闲时间而关闭。

func (db *DB) SetConnMaxLifetime

1
func (db *DB) SetConnMaxLifetime(d time.Duration)

设置连接的存活时间。过期的连接在复用之前可能会被延迟关闭。如果d <= 0,则连接不会因为连接的年龄而关闭。

func (db *DB) SetMaxIdleConns

1
func (db *DB) SetMaxIdleConns(n int)

设置空闲连接池中的最大连接数。如果MaxOpenConns大于0但小于新的MaxIdleConns,那么新的MaxIdleConns将被减少以匹配MaxOpenConns限制。

如果n <= 0,则不保留空闲连接。

缺省的最大空闲连接数是2。这可能会在未来的版本中改变。

func (db *DB) SetMaxOpenConns

1
func (db *DB) SetMaxOpenConns(n int)

设置到数据库的最大打开连接数。

如果MaxIdleConns大于0并且新的MaxOpenConns小于MaxIdleConns,那么MaxIdleConns将被减少以匹配新的MaxOpenConns限制。

如果n <= 0,则没有打开连接的数量限制。默认值是0(无限)。

func (db *DB) Stats() DBStats

1
func (db *DB) Stats() DBStats

Stats 返回数据库统计信息

关于 Conn 和 DB

Conn是一个连接,而DB是一个连接池,一般情况下使用DB就行,例如 db.Begin() ,查看源码实现可知:等同于 conn.BeginTx(Context.Background(), nil) ,后者需要手动调用conn.Close(),前者会自动调用conn.Close()

type DBStats

1
func (db *DB) Stats() DBStats
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type DBStats struct {
MaxOpenConnections int // Maximum number of open connections to the database.

// Pool Status
OpenConnections int // The number of established connections both in use and idle.
InUse int // The number of connections currently in use.
Idle int // The number of idle connections.

// Counters
WaitCount int64 // The total number of connections waited for.
WaitDuration time.Duration // The total time blocked waiting for a new connection.
MaxIdleClosed int64 // The total number of connections closed due to SetMaxIdleConns.
MaxIdleTimeClosed int64 // The total number of connections closed due to SetConnMaxIdleTime.
MaxLifetimeClosed int64 // The total number of connections closed due to SetConnMaxLifetime.
}

DBStats 包含数据库统计信息。

type IsolationLevel

1
2
3
4
5
6
7
8
9
10
11
12
type IsolationLevel int

const (
LevelDefault IsolationLevel = iota
LevelReadUncommitted
LevelReadCommitted
LevelWriteCommitted
LevelRepeatableRead
LevelSnapshot
LevelSerializable
LevelLinearizable
)

IsolationLevel 是 TxOptions 中使用的事务隔离级别。

BeginTx 中驱动程序可能支持的各种隔离级别。如果驱动程序不支持给定的隔离级别,则可能会返回错误。

https://en.wikipedia.org/wiki/Isolation_(database_systems)#Isolation_levels.

func (i IsolationLevel) String() string

1
func (i IsolationLevel) String() string

返回事务隔离级别的名称。

type NamedArg

1
2
3
4
type NamedArg struct {
Name string
Value interface{}
}

NamedArg 是一个命名参数。NamedArg 值可以用作 Query 或Exec 的参数,并绑定到 SQL 语句中的相应命名参数。

func Named

1
func Named(name string, value interface{}) NamedArg

Named 提供了一种更简洁的方式来创建 NamedArg 值。

示例:

1
2
3
4
5
6
7
8
db.ExecContext(ctx, `
delete from Invoice
where
TimeCreated < @end
and TimeCreated >= @start;`,
sql.Named("start", startTime),
sql.Named("end", endTime),
)

type NullBool

1
2
3
4
type NullBool struct {
Bool bool
Valid bool // Valid is true if Bool is not NULL
}

func (n *NullBool) Scan(value interface{}) error

func (n NullBool) Value() (driver.Value, error)

type NullByte

func (n *NullByte) Scan(value interface{}) error

func (n NullByte) Value() (driver.Value, error)

type NullFloat64

func (n *NullFloat64) Scan(value interface{}) error

func (n NullFloat64) Value() (driver.Value, error)

type NullInt16

func (n *NullInt16) Scan(value interface{}) error

func (n NullInt16) Value() (driver.Value, error)

type NullInt32

func (n *NullInt32) Scan(value interface{}) error

func (n NullInt32) Value() (driver.Value, error)

type NullInt64

func (n *NullInt64) Scan(value interface{}) error

func (n NullInt64) Value() (driver.Value, error)

type NullString

func (ns *NullString) Scan(value interface{}) error

func (ns NullString) Value() (driver.Value, error)

type NullTime

func (n *NullTime) Scan(value interface{}) error

func (n NullTime) Value() (driver.Value, error)

type Out

1
2
3
4
5
6
7
8
9
10
11
12
type Out struct {

// Dest is a pointer to the value that will be set to the result of the
// stored procedure's OUTPUT parameter.
Dest interface{}

// In is whether the parameter is an INOUT parameter. If so, the input value to the stored
// procedure is the dereferenced value of Dest's pointer, which is then replaced with
// the output value.
In bool
// contains filtered or unexported fields
}

Out 可用于从存储过程中检索 OUTPUT 值参数。

并非所有驱动程序和数据库都支持 OUTPUT 值参数。

示例:

1
2
var outArg string
_, err := db.ExecContext(ctx, "ProcName", sql.Named("Arg1", sql.Out{Dest: &outArg}))

type RawBytes

1
type RawBytes []byte

RawBytes 保存对数据库本身拥有的内存的引用。在扫描到RawBytes后,该切片只有在下一次调用next、Scan或Close时才有效。

type Result

1
2
3
4
5
6
7
8
9
10
11
12
13
type Result interface {
// LastInsertId returns the integer generated by the database
// in response to a command. Typically this will be from an
// "auto increment" column when inserting a new row. Not all
// databases support this feature, and the syntax of such
// statements varies.
LastInsertId() (int64, error)

// RowsAffected returns the number of rows affected by an
// update, insert, or delete. Not every database or database
// driver may support this.
RowsAffected() (int64, error)
}

Result 汇总了已执行的SQL语句。

type Row

1
2
3
4
5
type Row struct {
// One of these two will be non-nil:
err error // deferred error for easy chaining
rows *Rows
}

单行查询db.QueryRow()执行一次查询,并期望返回最多一行结果(即Row)。QueryRow总是返回非nil的值,直到返回值的Scan方法被调用时,才会返回被延迟的错误。(如:未找到结果)

func (r *Row) Err() error

func (r *Row) Scan(dest …interface{}) error

type Rows

1
2
3
type Rows struct {
// 包含过滤或未导出的字段
}

多行查询db.Query()执行一次查询,返回多行结果(即Rows),一般用于执行select命令。参数args表示query中的占位参数。

Rows is the result of a query. It’s cursor starts before the first row of the result set. Use Next to advance from row to row.

func (rs *Rows) Close() error

func (rs *Rows) ColumnTypes() ([]*ColumnType, error)

func (rs *Rows) Columns() ([]string, error)

func (rs *Rows) Err() error

func (rs *Rows) Next() bool

func (rs *Rows) NextResultSet() bool

func (rs *Rows) Scan(dest …interface{}) error

type Scanner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Scanner interface {
// Scan方法从数据库驱动获取一个值。
//
// 参数src的类型保证为如下类型之一:
// int64
// float64
// bool
// []byte
// string
// time.Time
// nil - 表示NULL值
//
// 如果不能不丢失信息的保存一个值,应返回错误。
Scan(src interface{}) error
}

Scanner接口会被Rows或Row等的Scan方法使用。

任何实现了Scanner接口的类型,都可以通过定义自己的Scan函数来处理空值问题,比如:

  • Rows或Row的Scan方法其实就是实现从数据库驱动中获取一个值(该值会转换成src的类型),并将其存储到src,src满足driver.Value类型
  • 而NullString、NullBool等的Scan方法则是将输入的值转换成对应的NullString、NullBool类型并存储下来

type Stmt ★★★

1
2
3
type Stmt struct {
// contains filtered or unexported fields
}

stmt = statement,声明,Stmt是一个准备好的声明。Stmt 对于多个 goroutines 并发使用是安全的。

func (s *Stmt) Close() error

func (s *Stmt) Exec(args …interface{}) (Result, error)

func (s *Stmt) ExecContext(ctx context.Context, args …interface{}) (Result, error)

func (s *Stmt) Query(args …interface{}) (*Rows, error)

func (s *Stmt) QueryContext(ctx context.Context, args …interface{}) (*Rows, error)

func (s *Stmt) QueryRow(args …interface{}) *Row

func (s *Stmt) QueryRowContext(ctx context.Context, args …interface{}) *Row

type Tx ★★★★★

1
2
3
type Tx struct {
// contains filtered or unexported fields
}

Tx 是正在进行的数据库事务。

Transaction 必须以对 Commit 或 Rollback 的调用结束。

在调用 Commit 或 Rollback 之后,事务上的所有操作都会因ErrTxDone而失败。

func (tx *Tx) Commit() error

func (tx *Tx) Exec(query string, args …interface{}) (Result, error)

func (tx *Tx) ExecContext(ctx context.Context, query string, args …interface{}) (Result, error)

func (tx *Tx) Prepare(query string) (*Stmt, error)

func (tx *Tx) PrepareContext(ctx context.Context, query string) (*Stmt, error)

func (tx *Tx) Query(query string, args …interface{}) (*Rows, error)

func (tx *Tx) QueryContext(ctx context.Context, query string, args …interface{}) (*Rows, error)

func (tx *Tx) QueryRow(query string, args …interface{}) *Row

func (tx *Tx) QueryRowContext(ctx context.Context, query string, args …interface{}) *Row

func (tx *Tx) Rollback() error

func (tx *Tx) Stmt(stmt *Stmt) *Stmt

func (tx *Tx) StmtContext(ctx context.Context, stmt *Stmt) *Stmt

type TxOptions

1
2
3
4
5
6
type TxOptions struct {
// Isolation is the transaction isolation level.
// If zero, the driver or database's default level is used.
Isolation IsolationLevel
ReadOnly bool
}

TxOptions保存要在DB.BeginTx中使用的事务选项。

Angular.js 项目所使用的git commit规范

每次提交,Commit message 都包括三个部分:Header,Body 和 Footer。其中,Header 是必需的,Body 和 Footer 可以省略

Header部分只有一行,包括三个字段:type(必需)、scope(可选)和subject(必需)

type

  • build:编译相关的修改,例如发布版本、对项目构建或者依赖的改动
  • chore:其他修改, 比如改变构建流程、或者增加依赖库、工具等
  • ci:持续集成修改
  • docs:文档修改
  • feat:新特性、新功能 feature
  • fix:修补bug
  • perf:优化相关,比如提升性能、体验
  • refactor:代码重构
  • revert:当前commit用于撤销以前的commit
  • style:代码格式修改,注意不是 css 修改
  • test:测试用例修改

[scope]

scope用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。

例如在Angular,可以是$location, $browser, $compile, $rootScope, ngHref, ngClick, ngView等。

如果你的修改影响了不止一个scope,你可以使用*代替。

subject

subject是 commit 目的的简短描述,不超过50个字符。

其他注意事项:

  • 以动词开头,使用第一人称现在时,比如change,而不是changed或changes
  • 第一个字母小写
  • 结尾不加句号(.)

Body

Body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

More detailed explanatory text, if necessary. Wrap it to about 72 characters or so. Further paragraphs come after blank lines. - Bullet points are okay, too - Use a hanging indent

注意点:

  • 使用第一人称现在时,比如使用change而不是changed或changes。
  • 永远别忘了第2行是空行
  • 应该说明代码变动的动机,以及与以前行为的对比。

Footer 部分只用于以下两种情况:

不兼容变动

如果当前代码与上一个版本不兼容,则 Footer 部分以BREAKING CHANGE开头,后面是对变动的描述、以及变动理由和迁移方法。

BREAKING CHANGE: isolate scope bindings definition has changed. To migrate the code follow the example below: Before: scope: { myAttr: ‘attribute’, } After: scope: { myAttr: ‘@’, } The removed inject wasn’t generaly useful for directives so there should be no code using it.

关闭 Issue

如果当前 commit 针对某个issue,那么可以在 Footer 部分关闭这个 issue 。

1
Closes #234

revert

如果当前 commit 用于撤销以前的 commit,则必须以revert:开头,后面跟着被撤销 Commit 的 Header。

revert: feat(pencil): add ‘graphiteWidth’ option

This reverts commit 667ecc1654a317a13331b17617d973392f415f02.

Body部分的格式是固定的,必须写成This reverts commit <hash>.,其中的hash是被撤销 commit 的 SHA 标识符。

如果当前 commit 与被撤销的 commit,在同一个发布(release)里面,那么它们都不会出现在 Change log 里面。如果两者在不同的发布,那么当前 commit,会出现在 Change log 的Reverts小标题下面。

举个例子

例如一个新功能,添加登陆模块 git commit 中的信息可以这样写:

1
feat[login] add login module

其中feat为提交类型type,login为域scope,“add login module”为 commit 目的的简短描述

git cz工具

1
2
3
4
5
# 全局安装
yarn global add commitizen

# 进入项目
commitizen init cz-conventional-changelog --save --save-exact

commitlint + husky

  1. 安装配置prettier

    1
    2
    3
    4
    5
    # 安装
    yarn add --dev --exact prettier
    # 配置
    .prettierrc.js
    .prettierignore
  2. 安装配置 Pre-commit Hook,用于提交代码之前自动格式化,这里会自动安装 husky

    1
    npx mrm@2 lint-staged
  3. 安装 eslint-config-prettier,解决prettier和eslint的冲突

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    yarn add eslint-config-prettier -D

    # vim package.json
    ...
    "eslintConfig": {
    "extends": [
    "react-app",
    "react-app/jest",
    "prettier"
    ]
    },
    ...
  4. 安装并配置 commitlint

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 1.
    yarn add @commitlint/{config-conventional,cli} -D
    # 2.
    echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
    # 3.
    cat <<EEE > .husky/commit-msg
    #!/bin/sh
    . "\$(dirname "\$0")/_/husky.sh"

    npx --no -- commitlint --edit "\${1}"
    EEE
    # 4.
    chmod a+x .husky/commit-msg

设置断点

设置条件断点

在汇编世界里,我们主要通过汇编指令来操纵两种东西:

  • 寄存器
  • 内存地址

通用寄存器

寄存器的位数

由于 x86-64 架构惊人的后向兼容性,同一个寄存器,我们可以使用其中的 8bit、16bit、32bit、64bit,以 ax 寄存器为例,分别是 ah/al、ax、eax、rax,如下图所示:

通用寄存器的逻辑结构一定要记住:

这就导致一个问题,从大的寄存器上往小的寄存器上 mov 没有问题,而从小的寄存器往大的寄存器上 mov 就需要使用 movsxd 或者 movsx 指令了:

寄存器有两种概念,逻辑上的和物理上的,分别是架构相关寄存器(architectural register)和物理寄存器(physical register)。前者是指令集(ISA)提供给编译器可见的,相当于 API 接口规范,一共 16 个通用寄存器;后者是硬件上实际设计的,软件领域不直接接触。最新的 CPU 可能有上百个实际的物理寄存器。当然了,对软件开发人员来说,我们只需要关注逻辑上的通用寄存器。

这 16 个逻辑上的通用寄存器如下所示:

寄存器 16bit / 32bit / 64bit 主要用途 编号
ax / eax / rax 累加器 0
cx / ecx / rcx 计数 1
dx / edx / rdx I/O 指针 2
bx / ebx / rbx DS 段的数据指针 3
sp / esp / rsp 堆栈指针寄存器在堆栈操作中使用,PUSH 和 POP 指令是从 SP 寄存器得到现行堆栈段的段内偏移量,所以称 SP 寄存器为堆栈指针:stack pointer,SP 始终指向栈顶。 4
bp / ebp / rbp BP 与 SS 连用,为访问现行堆栈段提供方便。通常 BP 寄存器在间接寻址中使用,操作数在堆栈段中,由 SS 段寄存器和 BP 组合形成操作数的地址,即 BP 中存放现行堆栈段中一个数据区的“基址”的偏移量,所以称 BP 为基址指针:base pointer 5
si / esi / rsi 变址寄存器(IndexRegister),它们主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。变址寄存器不可分割成 8 位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。它们可作一般的存储器指针使用。在字符串操作指令的执行过程中,对它们有特定的要求,而且还具有特殊的功能。
存储器指针,串指令中的源操作数指针:source indexRegister
6
di / edi / rdi 存储器指针,串指令中的目的操作数指针:destination indexRegister 7
r8
r9
r10
r11
r12
r13
r14
r15

编号一定要记住,后面有用

汇编就是在“寄存器与寄存器”或者“寄存器与内存”之间来回移动数据。

寄存器和内存编号(内存地址)的区别

没有区别,唯一的区别就是前者位于 cpu,后者位于内存

每个寄存器指向 16/32/64 位的空间

每个内存编号指向 8 位的空间,这也就是为什么说操作内存的最小单位是字节

常用指令

MOV 指令

1
2
3
4
5
6
7
8
9
10
11
MOV DST,SRC   ; DST:目标操作数		SRC:源操作数

MOV r/m8,r8 r 通用寄存器
MOV r/m16,r16 m 代表内存
MOV r/m32,r32 imm 代表立即数
MOV r8,r/m8 r8 代表8位通用寄存器
MOV r16,r/m16 m8 代表8位内存
MOV r32,r/m32 imm8 代表8位立即数
MOV r8, imm8
MOV r16, imm16
MOV r32, imm32

作用:拷贝源操作数到目标操作数

  1. 源操作数可以是立即数、通用寄存器、段寄存器、或者内存单元.
  2. 目标操作数可以是通用寄存器、段寄存器或者内存单元.
  3. 操作数的宽度必须一样.
  4. 源操作数和目标操作数不能同时为内存单元.

ADD 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ADD DST,SRC   ; DST:目标操作数		SRC:源操作数

ADD AL, imm8
ADD AX, imm16
ADD EAX, imm32
ADD r/m8, imm8
ADD r/m16,imm16
ADD r/m32,imm32
ADD r/m16, imm8
ADD r/m32, imm8
ADD r/m8, r8
ADD r/m16, r16
ADD r/m32, r32
ADD r8, r/m8
ADD r16, r/m16
ADD r32, r/m32

作用:目标操作数与源操作数相加,将结果存回目标操作数

示例:

1
2
3
mov eax,1
mov ebx,2
add eax,ebx ; eax = 3

SUB 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SUB DST,SRC

SUB AL, imm8
SUB AX, imm16
SUB EAX, imm32
SUB r/m8, imm8
SUB r/m16,imm16
SUB r/m32,imm32
SUB r/m16, imm8
SUB r/m32, imm8
SUB r/m8, r8
SUB r/m16, r16
SUB r/m32, r32
SUB r8, r/m8
SUB r16, r/m16
SUB r32, r/m32

作用:目标操作数减去源操作数,将结果存回目标操作数

示例:

1
2
3
mov eax,3
mov ebx,1
sub eax,ebx ; eax = 2

AND 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
AND DST,SRC

AND AL, imm8
AND AX, imm16
AND EAX, imm32
AND r/m8, imm8
AND r/m16,imm16
AND r/m32,imm32
AND r/m16, imm8
AND r/m32, imm8
AND r/m8, r8
AND r/m16, r16
AND r/m32, r32
AND r8, r/m8
AND r16, r/m16
AND r32, r/m32

作用:目标操作数和源操作数按位逻辑与操作,将结果存回目标操作数

示例:

1
2
3
mov eax,111  ; eax = 0x00000111 = 0000000000000000000000000000000111
mov ebx,101 ; ebx = 0x00000101 = 0000000000000000000000000000000101
and eax,ebx ; eax = 0x00000101

OR 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
OR DST,SRC

OR AL, imm8
OR AX, imm16
OR EAX, imm32
OR r/m8, imm8
OR r/m16,imm16
OR r/m32,imm32
OR r/m16, imm8
OR r/m32, imm8
OR r/m8, r8
OR r/m16, r16
OR r/m32, r32
OR r8, r/m8
OR r16, r/m16
OR r32, r/m32

作用:DST 和 SRC 按位逻辑或操作,将结果存回 DST

示例:

1
2
3
mov eax,100
mov ebx,1
or eax,ebx ; eax = 0x00000101

XOR 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
XOR DST,SRC

XOR AL, imm8
XOR AX, imm16
XOR EAX, imm32
XOR r/m8, imm8
XOR r/m16,imm16
XOR r/m32,imm32
XOR r/m16, imm8
XOR r/m32, imm8
XOR r/m8, r8
XOR r/m16, r16
XOR r/m32, r32
XOR r8, r/m8
XOR r16, r/m16
XOR r32, r/m32

作用:DST 和 SRC 按位逻辑异或操作,将结果存回 DST。异或是不同则真

示例:

1
2
3
mov eax,101
mov ebx,1
xor eax,ebx ; eax = 0x00000100

NOT 指令

1
2
3
4
5
not DST

NOT r/m8
NOT r/m16
NOT r/m32

作用:对 DST 按位取反,将结果存回 DST

示例:

1
2
3
4
mov eax,ffffffff
mov [56fee4],eax ; [56fee4] = 0xffffffff
not eax ; eax = 0x00000000
not dword ptr ds:[56fee4] ; [56fee4] = 0x00000000

LEA

Load Effective Address:加载有效地址,将源操作数的地址加载到目的寄存器中

MOV 指令可以通过内存地址读取数据,如果只是需要内存地址,就可以使用 LEA 指令

数据宽度和逻辑运算

圆 体现 正负数

cpu加法

示例:2+3

0010 xor 0011 = 0001

0010 & 0011 = 0010

0010 << 1 = 0100 判断0100是否等于0,如果等于,则结果为0001,显然不等于,则继续

0001 xor 0100 = 0101

0001 & 0010 = 0000 等于0 ,则结果为 0101,即5

示例:2-3

-3 = 14 = 1110

0010 xor 1101 = 1111

0010 & 1101 = 0000 << 1 = 0000 等于0

结果为1111 对应有符号的-1

总结:a + b

  1. a xor b = c1
  2. a & b << 1 = c2
  3. if c2 == 0 则结果为 c1,否则a = c1,b=c2,然后重复1、2、3,直到c2===0,返回c1

X86CPU 和 AMD64(X86-64)CPU 的区别:

  • 通用寄存器的位宽不同:AMD64 通用寄存器是 64 位,而 X86 通用寄存器是 32 位,所以 AMD64 一次能处理 64 位数据,而 X86 一次只能处理 32 位数据。

  • 支持的寻址空间不同:操作内存的最小单位是字节,每个内存地址(内存编号)指向一个字节,2^32 = 4294967296,所以 X86 支持的最大寻址空间就是 4294967296 字节,也就是 4GB。

  • AMD65 兼容 32 位操作系统,而 X86 不支持 64 位操作系统。

    • 32 位操作系统 内存地址是 32 位
    • 64 位操作系统 内存地址是 64 位

寄存器与内存

寄存器与内存的区别:

  1. 寄存器位于 CPU 内部,执行速度快,但比较贵。

  2. 内存速度相对较慢,但成本较低,所以可以做的很大。

  3. 寄存器和内存没有本质区别,都是用于存储数据的容器,都是定宽的。

  4. 寄存器常用的有 8 个:EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI。

  5. 计算机中的几个常用计量单位:BYTE WORD DWORD

    1
    2
    3
    BYTE 字节  8bit
    WORD 字 16bit
    DWORD 双字 32bit
  6. 内存的数量特别庞大,无法每个内存单元都起一个名字,所以用编号来代替,我们称计算机 CPU 是 32 位或者 64 位,主要指的就是内存编号的宽度,而不是寄存器的宽度。有很多书上说之所以叫 32 位计算机是因为寄存器的宽度是 32 位,是不准确的,因为还有很多寄存器是大于 32 位的。

    计算机内存的每一个字节会有一个编号(即内存编号的单位是字节),如下图:

    内存编号 编号对应的字节字节
    0x00000000 第 0 个字节
    0x00000001 第 1 个字节
    0x00000002 第 2 个字节
    ….
    ….
    ….
    0xFFFFFFFF 第 2^32 个字节,2^32=4294967296,4294967296BYTE = 4GB
  7. 32 位操作系统最多识别内存 4G,对吗?不对,通过 PAE 物理内存扩展技术,可以使处理器能够从 32 位寻址提升到 36 位,也就是说理论上最大的物理内存可达 64GB。

内存编号

内存格式

  1. 每个内存单元的宽度为 8
  2. [编号]称为地址
  3. 地址的作用:当我们想从内存中读取数据或者想向内存中写入数据,首先应该找到要读、写的位置。就像写信要写地址一样。

从指定内存中写入/读取数据

1
2
3
mov dword ptr ds:[0x0012FF34],0x12345678

mov eax,dword ptr ds:[0x0012FF34]
  • dword ptr:要读/写多少 此时是 4 字节 byte == 1 字节 word == 2 字节

    • ptr:代表后面是一个指针 (指针的意思就是里面存的不是普通的值,而是个地址)
  • []:中括号表示里面的数据是一个地址值,和 ptr 的作用一样

  • ds:段寄存器 先不用管 记住就行

  • 0x0012FF34:内存编号,必须是 32 位的 前面 0 可以省略

    注意:地址编号不要随便写,因为内存是有保护的,并不是所有的内存都可以直接读写(需要特别处理)

    建议地址编号写成 esp 的值

内存读写的 5 个公式

深刻理解内存寻址方式

寻址公式一 [立即数]

读取内存的值

1
MOV EAX,DWORD PTR DS:[0x13FFC4]  ; 将0x13FFC4中的值读出来,写到EAX中

向内存中写入数据

1
MOV DWORD PTR DS:[0x13FFC4],eax  ; 将eax中的值读出来,写到eax中

获取内存编号

1
2
3
4
LEA EAX,DWORD PTR DS:[0X13FFC4]  ; 将0X13FFC4这个内存编号(也就是0X13FFC4本身)写到EAX中

# LEA 与 MOV 的区别:
MOV EAX,DWORD PTR DS:[0X13FFC4] ; PTR表示后面是内存地址,MOV会读取内存地址中的值,而LEA只要这个内存地址

寻址公式二 [reg]

reg 代表寄存器,可以是 8 个通用寄存器中的任何一个

这种方式相比第一种,只是要先读取 reg

寻址公式三 [reg+立即数]

寻址公式四 [reg+reg*{1,2,4,8}]

用来寻址数组,有些数组元素占用不止一个内存单元,所以在计算数组元素的地址时需要用这种方式来计算:

1
数组元素地址 = 数组首地址 + 元素索引 * 数组元素占用空间

比如一个字形的数组,它的首地址是 6000H,那么它的第 4 个(索引从 0 开始)元素的地址就是:

1
6000H + 4 * 2 = 6008H

在高级的 CPU 中,这种比例因子的寻址方式是在指令级别上提供支持的,所以可以直接用这种方式寻址:

1
2
3
mov ebx,offset a
mov edx,4
mov ax,[ebx+edx*2]

寻址公式五 [reg+reg*{1,2,4,8}+立即数]

什么是堆栈

堆栈 在有些文章中也叫做 栈

可读、可写 这些都是针对正向的,编译器限制的,对于逆向而言,没有这些限制。

BASE 是栈底,TOP 是栈顶

栈底是高位,栈顶是低位,以 4G 内存为例,最高位 FFFFFFFF,最低位 00000000

栈底固定,栈顶不固定

RSP 和 RBP 通用寄存器

rsp 存储栈顶,rbp 存储栈底。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mov rdi,rsp
call 0x7f51189ef050


endbr64 # 0x7f51189ef050
push rbp
mov rbp,rsp
push r15
push r14
push r13
push r12
push rbx
sub rsp, 0x88
mov [rbp-0x88],rdi
rdtsc

...

堆栈操作

push

压栈,可以将寄存器 push 到堆栈,也可以将内存 push 到堆栈

1
2
3
4
5
PUSH r32
PUSH r16
PUSH m16
PUSH m32
PUSH imm8/imm16/imm32

push 最少 16 位,不能是 8 位

示例:

1
2
3
push rax   # 将rax复制到堆栈,然后rsp(栈顶)减8,为什么减而不是加,因为栈底在高位,栈顶在低位
push eax # 将eax复制到堆栈,然后esp(栈顶)减4
push ax # 将ax复制到堆栈,然后sp(栈顶)减2

pop

出栈,可以 pop 到寄存器,也可以 pop 到内存。

1
2
3
4
POP r32
POP r16
POP m16
POP m32

pop 最少 16 位,不能是 8 位

示例:

1
pop rax    # 将栈顶8个字节复制到rax,然后rsp加8

pushad

把当前寄存器全部 push 到栈中,其入栈顺序是:EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI

popad

POPAD 指令则是 PUSHAD 指令的逆操作。以此 pop 出保存的寄存器,顺序为 EDI、ESI、EBP、ESP、EBX、EDX、ECX、EAX.

pushad 和 popad 通常成对出现,作用 保护现场

先使用 pushad 把寄存器保存起来,然后就可以随便修改寄存器了(破坏现场),最后使用 popad 恢复保存的寄存器(恢复现场)

请问 SI,DI 的区别?
在用法上的区别?

最重要的是他们的默认段寄存器不同,SI 是 DS,DI 是 ES。


  1. PE
  2. 下断点
  3. WIN32 API
  4. 什么是函数调用
  5. 熟悉堆栈
  6. Call JCC 标志寄存器

一个函数开始执行的时候,栈顶存储的是这个函数的地址:

视频全屏黑一秒:

1
2
full-screen-api.transition-duration.enter  0 0
full-screen-api.transition-duration.leave 0 0