标准库

time

  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
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// 获取当前时间,这是个奇葩,必须是这个时间点, 据说是go诞生之日, 记忆方法:6-1-2-3-4-5
fmt.Println(time.Now().Format("2006-01-02 15:04:05"))

// 获取当天的年月日时间
now := time.Now()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
nowDayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
nowDayEnd := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 59, time.Local)

// 获取时间戳
timeUnix := time.Now().Unix()              //单位秒,10 位数
timeUnixMill := time.Now().UnixNano()/1e6  //单位毫秒,13 位数
timeUnixMicro := time.Now().UnixNano()/1e3 //单位微秒,16 位数
timeUnixNano := time.Now().UnixNano()      //单位纳秒,19 位数

// 时间戳转为日期格式
timestamp := time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")     // 秒级时间戳转换成日期
timestamp := time.Unix(int64(timestamp/1e3), 0).Format("2006-01-02 15:04:05") // 毫秒级时间戳转换成日期

// 获取7月1日的time
month7Start = time.Date(now.Year(), 7, 1, 0, 0, 0, 0, time.Local)

// ISO8601标准日期格式
time.Now().UTC().Format(time.RFC3339) // 世界统一时间:2024-09-20T07:01:44Z
time.Now().Format(time.RFC3339)       // 本地时区时间:2024-09-20T15:01:44+08:00

// 时间字符串转为 time.Time,再转时间戳 timeUnix := tTime.Unix()
startTime, _ := time.ParseInLocation("2006-01-02 15:04:05", "2024-02-28 10:24:26", time.Local)
func StringToTime(t string) (time.Time, error) {
    location, err := time.LoadLocation("Asia/Shanghai")
    if err != nil {
        return time.Time{}, err
    }
    tTime, _ := time.ParseInLocation("2006-01-02 15:04:05", t, location)
    return tTime, nil
}

// time.Time 转为时间字符串
func FormatTime(time time.Time) string {
    return time.Format("2006-01-02 15:04:05")
}

// time.RFC3339 时间格式化
func RFC3339ToTime(value string) (time.Time, error) {
    return time.ParseInLocation(time.RFC3339, value, time.Local)
}

// 秒级时间戳转为 time.Time
datetime := time.Unix(int64(1629107978), 0)

// 未来第30天
lastDay := time.Now().AddDate(0, 0, 30).Format("2006-01-02")

// 获取5分钟前的时间
time.Now().Add(-time.Minute * 5)

// 获取 1 小时之前的时间
m, _ := time.ParseDuration("-1h")
result := currentTime.Add(m)

// 睡眠 1 秒和 0.5 秒
time.Sleep(time.Second)
time.Sleep(time.Microsecond * 500)

// 两个时间点相差的天数
now := time.Now()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
futureDay := today.AddDate(0, 0, 1)
fmt.Printf("两个时间点相差的天数:%d\n", int(futureDay.Sub(today).Hours()/24)+1)

// 时间大小比较
now := time.Now()
a, _ := time.Parse("2006-01-02 15:04:05", "2019-03-10 11:00:00")
b, _ := time.Parse("2006-01-02 15:04:05", "2015-03-10 16:00:00")
fmt.Println("now a After: ",now.After(a))
fmt.Println("now a Before: ",now.Before(a))

// 时区设置,方法一:
var zone, _ = time.LoadLocation("Asia/Shanghai") // 地区标准时
fmt.Println(time.Now().In(zone).Format("2006-01-02 15:04:05"))

// 时区设置,方法二:
var zone = time.FixedZone("CST", 8*3600) // 东八区
fmt.Println(time.Now().In(zone).Format("2006-01-02 15:04:05"))

// 定时器: time.NewTimer 和 time.NewTicker
NewTimer: 只执行一次到时间执行
NewTicker: 循环执行只要定义完成从此刻开始计时不需要任何其他的操作每隔固定时间都会触发。『相比 sleep资源占用少
func timer() {
   timer := time.NewTimer(3 * time.Second)
   select {
   case <-timer.C:
      fmt.Println("3秒执行任务")
   }
   timer.Stop() // 这里来提高 timer 的回收
}
func ticker(activityId, userId uint64) (bool, error) {
    key := "keyxxx"
    var times uint32
    ticker := time.NewTicker(100 * time.Millisecond) // 定时器,定时往 channel 追加数据
    defer ticker.Stop()
    for {
        isLock := db.Redis(db.Default).SetNX(key, userId, 1*time.Minute).Val() // 利用 redis 加锁
        if isLock { // 是否是自身上锁
            return true, nil
        }

        // 避免用户等待时间过长
        times++
        if times > 100 {
            return false, errors.New("抽奖失败")
        }

        <-ticker.C // 定时器时间未到,阻塞,相当于 sleep
    }
}

encoding/json

1
2
3
4
5
6
7
8
// 输出json后的数据
data, _ := json.Marshal(userDevice)
fmt.Println(string(data))

// json解析成map并输出
var ret interface{}
json.Unmarshal(data, &ret)
fmt.Println(ret)

sync

  • sync.Mutex & sync.RWMutex
 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
# sync.Mutex互斥锁
type Mutex
    func (m *Mutex) Lock()
    func (m *Mutex) Unlock()

sync.RWMutex: 读写锁一般用在大量读操作少量写操作的情况

1. 同时只能有一个 goroutine 能够获得写锁定
2. 同时可以有任意多个 gorouinte 获得读锁定
3. 同时只能存在写锁定或读锁定读和写互斥)。

Mutex在大量并发的情况下会造成锁等待对性能的影响比较大而读写锁可以提高性能如果某个读操作的协程加了锁其他的协程没必要处于等待状态可以并发地访问共享变量这样能让读操作并行提高读性能

# Mutex 本身不支持可重入锁不要 copy 已使用的 Mutex避免死锁

可重入锁(或递归锁)这是 Java 并发包中非常常用的一个同步原语它的基本行为和互斥锁相同但是加了一些扩展功能当一个线程获取锁时如果没有其它线程拥有这个锁那么这个线程就成功获取到这个锁之后如果其它线程再请求这个锁就会处于阻塞等待的状态但是如果拥有这把锁的线程再请求这把锁的话不会阻塞而是成功返回所以叫可重入锁有时候也叫做递归锁)。只要你拥有这把锁你可以可着劲儿地调用比如通过递归实现一些算法调用者不会阻塞或者死锁 `划重点了:Mutex 不是可重入的锁。`
type Counter struct {
    sync.Mutex
    Count int
}

func main() {
    var c Counter
    c.Lock()
    defer c.Unlock()
    c.Count++
    foo(c) // 复制锁
}

// 这里Counter的参数是通过复制的方式传入的,导致死锁
func foo(c Counter) {
    c.Lock()
    defer c.Unlock()
    fmt.Println("in foo")
}

# 静态代码分析工具|死锁检测工具使用go自带的vet工具或者是第三方的工具比如go-deadlockgo-tools
go vet xxx.go
go tool vet source/directory/*.go // 一个包下所有源文件进行检测
go vet ./source/repository // 利用vet对一个package进行检查,当然传入的包名必须是 相对路径 或者完整package。
  • sync/atomic
 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
原子锁Go 语言的 sync/atomic 包提供了对原子操作的支持用于同步访问整数和指针

之所以叫原子操作是因为一个原子在执行的时候其它线程不会看到执行一半的操作结果在其它线程看来原子操作要么执行完了要么还没有执行就像一个最小的粒子 - 原子一样不可分割

* Go语言提供的原子操作都是非入侵式的
* 这些函数提供的原子操作共有五种增减比较并交换载入存储交换eg: atomic.AddUint32atomic.CompareAndSwapUint32
* 原子操作支持的类型类型包括int32int64uint32uint64uintptrunsafe.Pointer

# 原子操作与互斥锁的比较原子操作->乐观锁互斥锁->悲观锁
原子操作的做法趋于乐观总是假设被操作值未曾被改变并一旦确认这个假设的真实性就立即进行值操作那么在被操作值被频繁变更的情况下原子操作并不那么容易成功而使用互斥锁的做法则趋于悲观我们总假设会有并发的操作要修改被操作的值并使用锁将相关操作放入临界区中加以保护

# 应用场景
不涉及到对资源复杂的竞争逻辑只是会并发地读写这个标志这类场景就适合使用 atomic 的原子操作如定时任务是否运行的标志计数器

package main

import (
    "sync/atomic"
)

// 定时任务标识
var isRunning int32
func crontab() string {
    if !atomic.CompareAndSwapInt32(&isRunning, 0, 1) {
        return "该定时任务正在运行中..."
    } else {
        return "该定时任务空闲..."
    }
    defer func() {
        _ = atomic.CompareAndSwapInt32(&isRunning, 1, 0)
    }
}

// 计数器
func countAdd() {
    var ops uint64 = 0

    wg := sync.WaitGroup{}

    for i := 0; i < 50000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddUint64(&ops, 1)
        }()
    }

    wg.Wait()

    opsFinal := atomic.LoadUint64(&ops)
    fmt.Println("opsFinal:", opsFinal)
}
  • sync.WaitGroup
 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
// 使用sync.WaitGroup方式保证所有goroutine都运行完毕
func main() {
    wg := sync.WaitGroup{} // 初始化等待组
    wg.Add(100) // 初始化计数器,设置为100
    for i := 0; i < 100, i++ {
        go func(i int) {
            defer wg.Done() // 计数器-1
            fmt.Println(i)
        }(i)
    }
    wg.Wait() // 进程阻塞
}

# sync.WaitGroup对象不是一个引用类型在通过函数传值的时候需要使用地址
func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go f(i, &wg)
    }
    wg.Wait()
}
// 一定要通过指针传值,不然进程会进入死锁状态
func f(i int, wg *sync.WaitGroup) {
    fmt.Println(i)
    wg.Done()
}
  • sync.Map「并发安全」

使用原因:map是不支持并发操作的,只读是线程安全的,同时写线程不安全,所以为了并发安全 & 高效,请使用 sync.Map。「需要并发读写时,一般的做法是加锁,但这样性能并不高,故采用 sync.Map」

适用场景:并发处理 map,大量读,少量写。

 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
eg: var textMap sync.Map

* Store 写入
* Load 读取返回值有两个第一个是value第二个是bool变量表示key是否存在
* Delete 删除
* LoadOrStore 存在就读不存在就写
* Range 遍历注意遍历的快照

package main

import (
    "fmt"
    "sync"
)

func main() {
    var scene sync.Map

    // 将键值对保存到sync.Map
    scene.Store("greece", 97)
    scene.Store("london", 100)
    scene.Store("egypt", 200)

    // 从sync.Map中根据键取值
    fmt.Println(scene.Load("london"))

    // 根据键删除对应的键值对
    scene.Delete("london")

    // 遍历所有sync.Map中的键值对,返回的 bool 结果,true表明遍历继续,false表明遍历终止。
    scene.Range(func(k, v interface{}) bool {
        k2, ok := k.(string)
        if !ok {
            return true
        }
        fmt.Println("iterate:", k, v)
        return true
    })
}
  • sync.Once

sync.Once. Do(f func())是一个挺有趣的方法,能保证 f 方法只执行一次,无论循环调用了多少次 或 无论你是否更换 once. Do(xx) 这里的方法, 这个 sync.Once 块只会执行一次。

Once 是一个对象, 它提供了保证某个动作只被执行一次功能,最典型的场景就是单例模式。

 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
package main

import (
    "fmt"
    "sync"
    "time"
)

var once sync.Once
var once2 sync.Once

func main() {
    for i, v := range make([]string, 10) {
        once.Do(hello) // 只输出一次
        fmt.Println("count: ", v, "---", i)
    }

    for i := 0; i < 10; i++ {
        go func(i int) {
            once.Do(hello2) // 不会输出
            once2.Do(hello2) // 只输出一次
            fmt.Println("i: ", i)
        }(i)
    }

    time.Sleep(20)
}
func hello() {
    fmt.Println("hello world")
}
func hello2() {
    fmt.Println("hello world2222")
}
  • sync.Cond & sync.NewCond()

与互斥量不同,条件变量的作用并不是保证在同一时刻仅有一个线程访问某一个共享数据,而是在对应的共享数据的状态发生变化时,通知其他因此而被阻塞的线程。条件变量总是与互斥量组合使用。互斥量为共享数据的访问提供互斥支持,而条件变量可以就共享数据的状态的变化向相关线程发出通知。

 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
cond := sync.NewCond(new(sync.Mutex))

cond.L.Lock()
cond.L.Unlock()
cond.Wait() // Unlock()->阻塞等待通知(即等待Signal()或Broadcast()的通知)->收到通知->Lock()
cond.Signal() // 通知一个Wait()了的,若没有Wait(),也不会报错。Signal()通知的顺序是根据原来加入通知列表(Wait())的先入先出
cond.Broadcast() // 通知所有Wait()了的,若没有Wait(),也不会报错

# 案例
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    cond := sync.NewCond(new(sync.Mutex))
    condition := 0

    // Consumer
    go func() {
        for {
            cond.L.Lock()
            for condition == 0 {
                cond.Wait()
            }
            condition--
            fmt.Printf("Consumer: %d\n", condition)
            cond.Signal()
            cond.L.Unlock()
        }
    }()

    // Producer
    for {
        time.Sleep(time.Second)
        cond.L.Lock()
        for condition == 3 {
            cond.Wait()
        }
        condition++
        fmt.Printf("Producer: %d\n", condition)
        cond.Signal()
        cond.L.Unlock()
    }
}
  • sync.Pool

临时对象池,Pool 里装的对象可以无通知地被回收。它只提供了三个对外的方法:New、Get 和 Put。

作用:保存和复用临时对象,减少内存分配,降低GC压力。(Pool 是一个通用的概念,也是解决对象重用预先分配的一个常用的优化手段。)

有两个知识点需要记住:(1)sync.Pool 本身就是线程安全的,多个 goroutine 可以并发地调用它的方法存取对象;(2)sync.Pool 不可在使用之后再复制使用。

事实上,我们很少会使用 sync.Pool 去池化连接对象,原因就在于,sync.Pool 会无通知地在某个时候就把连接移除垃圾回收掉了,而我们的场景是需要长久保持这个连接,所以,我们一般会使用其它方法来池化连接。

我们一般不会在程序一开始的时候就开始考虑优化,而是等项目开发到一个阶段,或者快结束的时候,才全面地考虑程序中的优化点,而 Pool 就是常用的一个优化手段。如果你发现程序中有一种 GC 耗时特别高或系统中的 goroutine 数量非常多,有大量的相同类型的临时对象,不断地被创建销毁,这时,你就可以考虑看看,是不是可以通过池化的手段重用这些对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 定义临时对象池
var buffers = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

// 获取对象池中的对象实例:Get 方法的返回值还可能会是一个 nil,注意判断(Pool.New 字段没有设置,又没有空闲元素可以返回时,返回nil)
func GetBuffer() *bytes.Buffer {
    return buffers.Get().(*bytes.Buffer)
}

// 回收实例化对象:这个方法用于将一个元素返还给 Pool,Pool 会把这个元素保存到池中,并且可以复用。但如果 Put 一个 nil 值,Pool 就会忽略这个值。
func PutBuffer(buf *bytes.Buffer) {
    if buf.Cap() > 64<<10 { // 丢掉大buffer,减少内存泄露
        return
    }
    buf.Reset()
    buffers.Put(buf)
}

性能优化

一般做性能优化的时候,会采用对象池的方式,把不用的对象回收起来,避免被垃圾回收掉,这样使用的时候就不必在堆上重新创建了;

像数据库连接、TCP 的长连接,这些连接在创建的时候是一个非常耗时的操作。如果每次都创建一个新的连接对象,耗时较长,很可能整个业务的大部分耗时都花在了创建连接上;

如果能把这些连接保存下来,避免每次使用的时候都重新创建,不仅可以大大减少业务的耗时,还能提高应用程序的整体性能。

第三方对象池:oxtoacart/bpool

提供了以下几种类型的 buffer。

bpool.BufferPool: 提供一个固定元素数量的 buffer 池,元素类型是 bytes.Buffer,如果超过这个数量,Put 的时候就丢弃,如果池中的元素都被取光了,会新建一个返回。Put 回去的时候,不会检测 buffer 的大小。

bpool.BytesPool:提供一个固定元素数量的 byte slice 池,元素类型是 byte slice。Put 回去的时候不检测 slice 的大小。

bpool.SizedBufferPool: 提供一个固定元素数量的 buffer 池,如果超过这个数量,Put 的时候就丢弃,如果池中的元素都被取光了,会新建一个返回。Put 回去的时候,会检测 buffer 的大小,超过指定的大小的话,就会创建一个新的满足条件的 buffer 放回去。

bpool 最大的特色就是能够保持池子中元素的数量,一旦 Put 的数量多于它的阈值,就会自动丢弃,而 sync.Pool 是一个没有限制的池子,只要 Put 就会收进去。

bpool 是基于 Channel 实现的,不像 sync.Pool 为了提高性能而做了很多优化,所以,在性能上比不过 sync.Pool。 不过,它提供了限制 Pool 容量的功能,所以,如果你想控制 Pool 的容量的话,可以考虑这个库。

  • bytes.buffer

bytes.buffer是一个缓冲byte类型的缓冲器

flag 或 os. Args[]

用于解析命令行选项,如

 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
package main

import (
    "flag"
    "fmt"
)

/**
 * flag 设置命令行选项
 * go run flag.go -h
 * go run flag.go -s hello
 * go run flag.go -i 1 -s hello -b true
 * go run flag.go -i 1 -b true -s hello // 这里发现个问题:-b 只能放在最后一个选项,否则会获取不到值
 */

var (
    i   *int
    b   *bool
    s   *string
)

func init() {
    i = flag.Int("i", 0, "int flag value")
    b = flag.Bool("b", false, "bool flag value")
    s = flag.String("s", "default", "string flag value")
}

func main() {
    flag.Parse()

    fmt.Println("int flag:", *i)
    fmt.Println("bool flag:", *b)
    fmt.Println("s flag:", *s)
}

errors包 或 fmt. Errorf()

1
2
3
4
5
6
7
type error interface {
    Error() string
}

错误处理fmt.Errorf("math: square root of negative number %g", f)

Go 没有像 Java  .NET 那样的 try/catch 异常机制不能执行抛异常操作但是有一套 defer-panic-and-recover 机制

net, net/http, net/url

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 网络通讯包Go  http 包兼具 HTTP 服务器和 HTTP 客户端的功能HTTP 客户端支持 GET/POST/PUT 等请求方式常用于访问网页或者请求第三方 API 
http.HandleFunc("/", sayHelloName) // 路由注册
err := http.ListenAndServe(":8080", nil) // TCP 端口监听,对每个请求实例化一个 Conn,并且开启一个 goroutine 为这个请求进行服务 go c.serve ()
resp, err = http.Get(baseURL + "/") // http请求

# net/url 专门处理 URL
// url 编码和解码
url.QueryEscape(s string) string // urlencode
url.QueryUnescape(s string) (string, error) // urldecode

// url 编码,仅适用于字符串参数
var urlStr url.URL
q := urlStr.Query()
q.Add("name", "张三")
q.Add("age", "20")
q.Add("sex", "1")
queryStr := q.Encode()
fmt.Println(queryStr) // 输出:age=20&name=%E5%BC%A0%E4%B8%89&sex=1

html/template

1
2
3
4
// 字符转义
fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) // 输出到服务器端
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))
template.HTMLEscape(w, []byte(r.Form.Get("username"))) // 输出到客户端

strings & strconv

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 将一个[]string的切片通过分隔符,分割成一个字符串
s := []string{"hello", "word", "xiaowei"}
ret := strings.Join(s, "-")

// 把字符串按照指定的分隔符切割成slice
ret := strings.Split("a,b,c,d,e", ",")

string([]byte) // 坑:string()只能将[]byte类型的数据转换为字符串

// 所有单次首字母大写
fmt.Println(strings.Title("hello world")) // 输出 Hello World

sort

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 数组排序
array1 := []int{1, 5, 2, 8, 9, 4, 10}
sort.Ints(array1)

// 下面实现排序order by age asc, name desc,如果 age 和 name 都相等则保持原始排序
sort.SliceStable(family, func(i, j int) bool {
    if family[i].Age != family[j].Age {
        return family[i].Age < family[j].Age // 返回true则不用交换
    }
    return strings.Compare(family[i].Name, family[j].Name) == 1
})

// 注:sort.SliceStable->稳定排序:假定在待排序的序列中存在多个具有相同值的元素,若经过排序,这些元素的相对次序保持不变,即在原序列中,若r[i]=r[j]且r[i]在r[j]之前,在排序后的序列中,若r[i]仍在r[j]之前,则称这种排序算法是稳定的(stable);否则称为不稳定的。

context

context是Go中广泛使用的程序包,由Google官方开发,在1.7版本引入。它用来简化在多个 goroutine 传递上下文数据、(手动/超时)中止 goroutine 等操作.

context库中,有4个关键方法:

- context.WithValue() 可以设置一个key/value的键值对,可以在下游任何一个嵌套的context中通过key获取value。但是不建议使用这种来做goroutine之间的通信。
- context.WithCancel() 返回一个cancel函数,调用这个函数则可以主动停止goroutine。
- context.WithTimeout() 函数可以设置一个time.Duration,到了这个时间则会cancel这个context。
- context.WithDeadline() 该函数跟WithTimeout很相近,只是WithDeadline设置的是一个时间点。

Context 中实现了 2 个常用的生成顶层 Context 的方法。

- context.Background(): 其返回值是一个空的context,经常作为树的根结点,它一般由接收请求的第一个 goroutine 创建,不能被取消、没有值、也没有过期时间。
- context.TODO(): 应该仅在不确定应该使用哪种上下文时使用。(从源代码来看,context.Background 和 context.TODO 也只是互为别名,没有太大的差别)

对于路由层来说,不要将 context 传入 goroutine 中,因为路由结束后,context 可能会 Cancel 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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
type Context interface {
    Deadline() (deadline time.Time, ok bool) // Deadline 方法会返回这个 Context 被取消的截止日期。如果没有设置截止日期,ok 的值是 false。后续每次调用这个对象的 Deadline 方法时,都会返回和第一次调用相同的结果。
    Done() <-chan struct{} // Done 方法返回一个 Channel 对象。在 Context 被取消时,此 Channel 会被 close,如果没被取消,可能会返回 nil。后续的 Done 调用总是返回相同的结果。当 Done 被 close 的时候,你可以通过 ctx.Err 获取错误信息。Done 这个方法名其实起得并不好,因为名字太过笼统,不能明确反映 Done 被 close 的原因,因为 cancel、timeout、deadline 都可能导致 Done 被 close,不过,目前还没有一个更合适的方法名称。
    Err() error // 如果 Done 没有被 close,Err 方法返回 nil;如果 Done 被 close,Err 方法会返回 Done 被 close 的原因。
    Value(key interface{}) interface{} // Value 返回此 ctx 中和指定的 key 相关联的 value。
}

// 示例
func main() {
    // Value() 用法
    ctx := context.TODO()
    ctx = context.WithValue(ctx, "key1", "0001")
    ctx = context.WithValue(ctx, "key2", "0002")
    fmt.Println(ctx.Value("key1"), ctx.Value("key2"))

    // cancel
    ctx, cancel := context.WithCancel(context.Background())
    go work(ctx, "work1")
    time.Sleep(time.Second * 3)
    cancel() // 调用这个函数则可以主动停止goroutine
    time.Sleep(time.Second)

    // timeout超时处理
    ctx2, timeCancel := context.WithTimeout(context.Background(), time.Second*1)
    defer timeCancel()
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop()
    for {
        select {
        case <-ctx2.Done(): // 1s 后退出 select 循环和 for 循环
            return ctx2.Err()
        case <-ticker.C: // 500ms 后退出 select 循环,继续走 for 循环
        }

        fmt.Println("xxx") // 输出 2 次
    }

    // deadline
    ctx3, deadlineCancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*3))
    go work(ctx3, "deadline cancel")
    time.Sleep(time.Second * 2)
    deadlineCancel() // 调用此方法会提前中断 goroutine,不调用则按设置的超时时间中断 goroutine

    time.Sleep(time.Second * 3)
}

func work(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            println(name, " get message to quit")
            return
        default:
            println(name, " is running", time.Now().String())
            time.Sleep(time.Second)
        }

    }
}

os/signal

包含两个方法:一个是 notify 方法用来监听收到的信号;一个是 stop 方法用来取消监听。

1
2
3
# 优雅退出go守护进程
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)

信号(Signal)是Linux, 类Unix和其它POSIX兼容的操作系统中用来进程间通讯的一种方式。一个信号就是一个异步的通知,发送给某个进程,或者同进程的某个线程,告诉它们某个事件发生了。 当信号发送到某个进程中时,操作系统会中断该进程的正常流程,并进入相应的信号处理函数执行操作,完成后再回到中断的地方继续执行。

信号 动作 说明
SIGINT 2 Term 用户发送INTR字符(Ctrl+C)触发
SIGKILL 9 Term 无条件结束程序(不能被捕获、阻塞或忽略)
SIGTERM 15 Term 结束程序(可以被捕获、阻塞或忽略)

math/rand

1
2
3
4
rand.Seed(time.Now().UnixNano()) // 核心:设置随机种子,保证产生随机数

fmt.Println(rand.Float64())
fmt.Println(rand.Intn(100))

第三方库

go-nsq

消息队列,支持异步队列

github.com/hibiken/asynq

go-redis

go-redis文档

 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
func main() {
    redisClient, err := initClient()
    if err != nil {
        //redis连接错误
        panic(err)
    }

    // Set 第三个参数代表key的过期时间,0代表不会过期。
    err = redisClient.Set("name1", "zhangsan", 0).Err()
    if err != nil {
        panic(err)
    }

    // Result函数返回两个值,第一个是key的值,第二个是错误信息
    var val string
    val, err = redisClient.Get("name1").Result()
    // 判断查询是否出错
    if err != nil {
        panic(err)
    }
    fmt.Println("name1的值:", val) //name1的值:zhangsan

    // 判断 key 是否存在
    val, err := redisClient.Get("user:zhangyunfeiVir").Result()
    if err == redis.Nil {
        fmt.Println("key does not exist")
    } else if err != nil {
        panic(err)
    } else {
        fmt.Println("读取:", val)
    }

    // SetNx
    if redisClient.SetNX("cache_key", '1', time.Minute).Val() == false  {
        return errors.New("请求过快,请稍后重试")
    }
}

go-admin

后台管理系统

validator

表单校验

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// validator会自动校验struct内定义的变量的数据类型
type createVoteRequest struct {
    ChatroomId   uint64 `form:"chatroom_id" json:"chatroom_id" validate:"required"` // 会自动校验uint64这个数据类型
    Title        string `form:"title" json:"title" validate:"required,max=100"`
    IsMultiple   uint8 `form:"is_multiple" json:"is_multiple" validate:"oneof=0 1"`
    DeadlineTime string `form:"deadline_time" json:"deadline_time" validate:"required,len=16"`
    Options      []string `form:"options" json:"options" validate:"required,min=1,max=7,dive,min=1,max=7"` // 切片最少1个最多7个,切片内的值最小为1最大为7
    KeyWordType int8   `json:"key_word_type" form:"key_word_type" query:"key_word_type" validate:"required_with=KeyWord"`
    KeyWord     string `json:"key_word" form:"key_word" query:"key_word"`
}

dive: 告诉验证者深入到`切片`并使用后面的验证标签验证切片的级别. 还支持多维嵌套您要潜水的每个级别都将需要另一个潜水标签. 潜水有一些子标签"键""端键"

required: 这可以验证该值不是数据类型默认的零值. 对于数字请确保值不为零. 对于字符串请确保值不是"". 对于切片映射指针接口通道和函数请确保该值不为nil.

oneof: 对于字符串整数和整数oneof将确保该值是参数中的值之一. 该参数应该是由空格分隔的值列表. 值可以是字符串或数字.
这个验证器可以用于onef=0 1这样的判断

omitempty: 允许条件验证例如如果该字段没有设置值"必需"验证器确定),则其他验证例如min或max将不会运行但是如果设置了值验证将运行.

required_with关联矫正如required_with=Name 若Name字段不为零值则当前字段不能为零值Field validate:"required_with=Field1 Field2"

required_without其他字段其中一个为空则当前字段不能为空 Field `validate:required_without=Field1 Field2

github.com/tidwall/gjson

直接对 json 字符串进行数据进行读取,不用提前 json 解析

1
2
3
4
5
// 遍历数组
result := gjson.Get(json, "programmers.#.lastName")
for _, name := range result.Array() {
	println(name.String())
}
 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
package main

import "github.com/tidwall/gjson"

const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}`
func main() {
    value := gjson.Get(json, "name.last")
    println(value.String())
}

// 解析远程json,并进行原始排序,因为 json 解析后的 map 是无序的
func parse() ([]string, error) {
    res, err := http.Get("http://fw-wxa.yidejia.com/city.json")
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()
    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }
    jsonData := gjson.Parse(strings.Replace(string(body), "'", "\"", -1))

    tempMapKeysIndex := make(map[string]int)
    provinces := make([]string, 0)

    for k, _ := range jsonData.Map() {
        fmt.Println("省份:", k, "省份所在 json 位置(可用于 map 的排序):", jsonData.Get(k).Index)

        provinces = append(provinces, k)
        tempMapKeysIndex[k] = jsonData.Get(k).Index

        // for kk, _ := range v.Map() {
        //     fmt.Println("市:", kk, "省所在 json 位置(可用于 map 的排序):", jsonData.Get(kk).Index)
        // }
    }

    sort.Slice(provinces, func(i, j int) bool {
        return tempMapKeysIndex[provinces[i]] < tempMapKeysIndex[provinces[j]]
    })

    return provinces, nil
}

github.com/tidwall/sjson

直接对 json 字符串进行数据赋值,不用提前 json 解析

1
2
3
4
5
6
7
8
9
package main

import "github.com/tidwall/sjson"

const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}`
func main() {
    value, _ := sjson.Set(json, "name.last", "Anderson")
    println(value)
}

github.com/dgrijalva/jwt-go

github.com/ahmetb/go-linq

1
2
3
4
5
// slice获取某列字段数据
var chatroomIds []uint64
linq.From(chatrooms).Select(func(i interface{}) interface{} {
    return i.(model.Chatroom).Id
}).ToSlice(&chatroomIds)

go.uber.org/zap

uber开源的日志库zap,对性能和内存分配做了极致的优化。[参考文章:https://www.liwenzhou.com/posts/Go/zap/]

github.com/panjf2000/ants/v2

ants是一个高性能的 goroutine 池,实现了对大规模 goroutine 的调度管理、goroutine 复用,允许使用者在开发并发程序的时候限制 goroutine 数量,复用资源,达到更高效执行任务的效果。

uber-go/dig

依赖注入,接口抽象化

Go 的依赖注入

github.com/cosmtrek/air

代码修改后需重新编译才能看到变更,这为我们本地开发带来了诸多不便,故需要有自动重载方案,提高开发效率。

自动重载方案,意味着一个 main 文件关联代码变更后,不需要手动杀掉再执行 go run

github.com/stretchr/testify

这是一个知名的第三方测试包,我们将用到他的断言(Assertion)功能。

1
2
3
// 示例
assert.Equal(t, 200, resp.StatusCode, "应返回状态码 200")
assert.NoError(t, err, "有错误发生,err 不为空")

github.com/judwhite/go-svc

svc: service,svc服务运行框架,用于实现守护进程,守护进程(daemon)是在后台运行的特殊进程,用于执行指定的系统任务,一直运行直到系统关闭。

NSQ 源码的程序入口,是通过 go-svc 包启动为守护进程。

 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
// 启动进程入口示例
package main

import (
    "github.com/judwhite/go-svc"
    _ "net/http/pprof"
    "os"
    "path/filepath"
    "sync"
    "syscall"
    "xxx/config"
    "xxx/lib/db"
    "xxx/lib/logger"
)

type logicProgram struct {
    once sync.Once
}

func main() {
    p := &logicProgram{}
    if err := svc.Run(p, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL); err != nil {
        logger.Sugar.Fatal(err)
    }
}

// svc 服务运行框架 程序启动时执行Init+Start, 服务终止时执行Stop
func (p *logicProgram) Init(env svc.Environment) error {
    if env.IsWindowsService() {
        dir := filepath.Dir(os.Args[0])
        return os.Chdir(dir)
    }
    return nil
}

func (p *logicProgram) Start() error {
    mysqlConnCount := 10

    db.ConnectMysql(config.MySQL, "database_name", mysqlConnCount)

    db.ConnectRedis(config.RedisIP, config.RedisPassword, 0, "default")

    logger.Sugar.Info("start logic...")
    return nil
}

func (p *logicProgram) Stop() error {
    p.once.Do(func() {
        logger.Sugar.Info("stop logic server")
        defer db.DisconnectMysql()
        defer db.DisconnectRedis()
    })
    return nil
}

github.com/micro/micro/v3

微服务框架

github.com/tal-tech/go-zero

go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。

github.com/robfig/cron || github.com/go-co-op/gocron

定时任务

github.com/tal-tech/go-stash

go-stash是一个高效的从Kafka获取数据,根据配置的规则进行处理,然后发送到ElasticSearch集群的工具。

go-stash有大概logstash 5倍的吞吐性能,并且部署简单,一个可执行文件即可。

github.com/fsnotify/fsnotify

可以监听文件修改进而自动重新加载,可用于实现配置热更新。

fsnotify的使用比较简单:

  1. 先调用NewWatcher创建一个监听器;
  2. 然后调用监听器的Add增加监听的文件或目录(如果是实现配置热更新:后端收到事件后读取整个文件内容,再判断哪个参数变更了,最后该变量引用赋值);
  3. 如果目录或文件有事件产生,监听器中的通道Events可以取出事件。如果出现错误,监听器中的通道Errors可以取出错误信息。

go.uber.org/zap

日志记录

github.com/ChimeraCoder/gojson

根据 json 生成 go 文件

github.com/PuerkitoBio/goquery

网页爬虫工具,goquery的选择器功能很强大,很好用。

github.com/cenkalti/backoff

操作重试 Golang - Backoff+Ticker+Timer示例

操作重试有不同的策略(退避算法),同时操作重试在很场景中都使用到了,比如IP数据包发送(CSMA/CD)、网络通信,RPC服务调用等,有的是用于协调网络传输速率,避免网络拥塞,有的是为了考虑网络波动影响,提高服务可用性;

常见的是 指数退避算法 ,通常起先是基于一个较低的时间间隔尝试操作,若尝试失败,则按指数级的逐步延长事件发生的间隔时间,直至超过最大尝试机会;大多数指数退避算法会利用抖动(随机延迟)来防止连续的冲突。

退避算法有两块,一块是退避策略,即间隔多久操作下一次(退避策略);另一块是累计可以支持的最大操作次数(重试次数)

1
2
3
4
5
6
// 使用指数退避算法策略示例(失败后,基于指数级别的间隔,再次发起操作)
syncToken := func() error { ... }
retryer := backoff.WithContext(backoff.NewExponentialBackOff(), ctx)
if err := backoff.Retry(syncToken, retryer); err != nil {
    fmt.Println("retry getting access token failed", "err", err)
}

http client

fasthttp

resty

github.com/totoval/totoval

API web framework:Totoval 是一个可以帮助 Go 工程师快速、便利、安全构建一个性能成熟项目的 API Web 框架

github.com/miku/zek

Zek is a prototype for creating a Go struct from an XML document.

github.com/go-yaml/yaml

.yaml 文件处理

uuid

UUID 版本迭代

  1. v1 版本说明 v1 是基于当前时间戳、机器 MAC 地址生成的,因为 MAC 地址是全球唯一的。从而保证 UUID 唯一,这种方式其实暴露了 MAC 地址和生成时间。
  2. v2版本说明 基于时间的 UUID 算法相同,会把时间戳的前4位换成 POSIX 的UID 和GID。【纯数字】
  3. v3版本说明 用户指定了一个命名空间和一个具体字符串, 然后通过 MD5散列来生成 UUID。
  4. v4基于随机数 根据随机数或者伪随机数生成 UUID, 这个版本用的比较多。

github.com/rs/xid:一般场景推荐使用,其性能好、字符串较短

1
2
3
import "github.com/rs/xid"

fmt.Println(xid.New().String()) // 输出:cahg45plicstuuog5590【占12字节,20个字符】

github.com/google/uuid

1
2
3
4
import "github.com/google/uuid"

fmt.Println(uuid.New().String()) // uuid v4 输出:4738c1d9-04ce-46a5-a49f-7cc2b9f061e2【占16字节,36个字符】
fmt.Println(uuid.New().ID()) // uuid v2 输出:755714972

github.com/coocood/freecache

freecache 是一个用 go 语言实现的本地缓存系统(类似于 lru)。相关的 github 地址:https://github.com/coocood/freecache

它有几个特性值得注意:

  • 通过优秀的内存管理方案,实现了 go 语言的零 gc
  • 是线程安全的,同时支持一定程度的并发,非常适合并发场景
  • 支持设置失效时间,动态失效过期缓存
  • 在一定程度上支持 lru,即“最近最少使用”,会在容量不足的时候优先淘汰较早的数据

这几个优秀特性使得他非常适合用在生产环境中作为本地缓存。

freecache + etcd watch():可实现分布式本地缓存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 简单使用示例
cacheSize := 100 * 1024 * 1024
cache := freecache.NewCache(cacheSize)
debug.SetGCPercent(20)
key := []byte("abc")
val := []byte("def")
expire := 60 // expire in 60 seconds
cache.Set(key, val, expire)
got, err := cache.Get(key)
if err != nil {
    fmt.Println(err)
} else {
    fmt.Println(string(got))
}
affected := cache.Del(key)
fmt.Println("deleted key ", affected)
fmt.Println("entry count ", cache.EntryCount())

github.com/jinzhu/copier

I am a copier, I copy everything from one to another

github.com/yanyiwu/gojieba

gojieba 是一个高性能的中文分词库,非常适合做文本分析,文本搜索等业务;它的计算分词过程,词典载入过程都非常快;gojieba 底层代码都由 C++ 封装而来,比原生 Go 拥有更高的性能,但在之 gojieba 上二次扩展开发不是很便利,满足需求的情况推荐使用。

github.com/Lofanmi/chinese-calendar-golang/calendar

 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
func main() {
    birthday := time.Date(1983, 10, 31, 0, 0, 0, 0, time.Local)

    fmt.Println(GetLunarDate(birthday))
    fmt.Println(GetAge(GetLunarDate(birthday)))

    fmt.Println(GetSolarDate(birthday))
    fmt.Println(GetAge(GetSolarDate(birthday)))
}

// 根据时间获取农历的年月日
func GetLunarDate(t time.Time) (year int, month int, day int) {
    if t.IsZero() {
        return 0, 0, 0
    }
    c := calendar.BySolar(int64(t.Year()), int64(t.Month()), int64(t.Day()), int64(t.Hour()), int64(t.Minute()), int64(t.Second()))
    return int(c.Lunar.GetYear()), int(c.Lunar.GetMonth()), int(c.Lunar.GetDay())
}

// 根据时间获取阳历的年月日
func GetSolarDate(t time.Time) (year int, month int, day int) {
    if t.IsZero() {
        return 0, 0, 0
    }
    c := calendar.ByLunar(int64(t.Year()), int64(t.Month()), int64(t.Day()), int64(t.Hour()), int64(t.Minute()), int64(t.Second()), false)
    return int(c.Solar.GetYear()), int(c.Solar.GetMonth()), int(c.Solar.GetDay())
}

// 算年龄
func GetAge(year, month, day int) (age int) {
    age = time.Now().Year() - year
    if int(time.Now().Month()) < month {
        age--
    } else if time.Now().Day() < day {
        age--
    }
    if age < 0 {
        age = 0
    }
    return
}

github.com/shopspring/decimal

解决 float(浮点数) 精度丢失问题

 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
// Round 四舍五入
// 返回将 val 根据指定精度 precision(十进制小数点后数字的数目)进行四舍五入的结果,precision可为0
func Round(val float64, precision int) float64 {
    p := math.Pow10(precision)
    return math.Floor(val*p+0.5) / p
}

// 元转分
func Yuan2Fen(f float64) int64 {
    if f == 0 {
        return 0
    }
    decimalValue := decimal.NewFromFloat(f)
    decimalValue = decimalValue.Mul(decimal.NewFromInt(100))
    return decimalValue.BigInt().Int64()
}

// 分转元
func Fen2Yuan(i int64) string {
    if i == 0 {
        return "0.00"
    }
    decimalValue := decimal.NewFromInt(i)
    decimalValue = decimalValue.Div(decimal.NewFromInt(100))
    return decimalValue.String()
}

// 元转分
func Yuan2FenString(s string) int64 {
    f, _ := strconv.ParseFloat(s, 10)
    if f == 0 {
        return 0
    }
    decimalValue := decimal.NewFromFloat(f)
    decimalValue = decimalValue.Mul(decimal.NewFromInt(100))
    return decimalValue.BigInt().Int64()
}

// 两个浮点数相乘
func FloatMulFloat(f1, f2 float64) float64 {
    decimalValue1 := decimal.NewFromFloat(f1)
    decimalValue2 := decimal.NewFromFloat(f2)
    decimalValue := decimalValue1.Mul(decimalValue2)
    ret, _ := decimalValue.Float64()
    return ret
}

// 两个浮点数相减
func FloatSubFloat(f1, f2 float64) float64 {
    decimalValue1 := decimal.NewFromFloat(f1)
    decimalValue2 := decimal.NewFromFloat(f2)
    decimalValue := decimalValue1.Sub(decimalValue2)
    ret, _ := decimalValue.Float64()
    return ret
}

os/exec

 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
// 获取音频时长
func GetAudioDuration(url, filepath, ffmpegPath, bashPath string) (float64, error) {
    var second float64
    if err := DownLoad(url, filepath); err != nil {
        return second, err
    }

    // 复杂命令,如多管道,必须使用 bash -c 来执行
    cmd := exec.Command(bashPath, "-c", ffmpegPath+" -i "+filepath+" 2>&1 | grep 'Duration' | cut -d ' ' -f 4 | sed s/,//")
    out, err := cmd.CombinedOutput()
    if err != nil {
        return second, errors.New("cmd err: " + err.Error() + "->" + string(out))
    }
    durationStr := strings.TrimSpace(string(out))
    durationArr := strings.Split(durationStr, ":")
    if len(durationArr) != 3 {
        return second, nil
    }
    h, _ := strconv.ParseFloat(durationArr[0], 10)
    m, _ := strconv.ParseFloat(durationArr[1], 10)
    s, _ := strconv.ParseFloat(durationArr[2], 10)
    second = h*3600 + m*60 + s
    return second, nil
}

func TestGetAudioDuration(t *testing.T) {
    var (
        audioUrl   = "https://xxx.com/clt66e50dqqmur9ctveg.wav"
        filepath   = fmt.Sprintf("/Users/xxx/Downloads/%s.wav", uuid.New().String())
        ffmpegPath = "/usr/local/bin/ffmpeg"
        bashPath   = "/bin/bash"
    )
    second, err := GetAudioDuration(audioUrl, filepath, ffmpegPath, bashPath)
    assert.NoError(t, err)
    t.Log(second)
}

// 获取音频时长-本地文件(eg:ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 /Users/xxx/Downloads/xxx.mp4)
func GetMediaSizeLocal(filepath, ffprobePath, bashPath string) (int64, int64, error) {
    var width, height int64

    cmd := exec.Command(bashPath, "-c", ffprobePath+" -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 "+filepath)
    out, err := cmd.CombinedOutput()
    if err != nil {
        return width, height, errors.New("cmd err: " + err.Error() + "->" + string(out))
    }
    sieStr := strings.TrimSpace(string(out))
    sizeArr := strings.Split(sieStr, ",")
    if len(sizeArr) < 2 {
        return width, height, nil
    }
    width, _ = strconv.ParseInt(sizeArr[0], 10, 64)
    height, _ = strconv.ParseInt(sizeArr[1], 10, 64)
    return width, height, nil
}