前言

在使用 go 语言进行开发时,经常会遇到一些常见的编程任务和问题。为了提高开发效率和代码质量,造轮子是很常见的行为,使用轮子能提高我们的开发效率和代码复用率。

这篇文章主要汇总一下我在开发过程中自己写的一些轮子,供大家参考使用。

JWT

JWT(JSON Web Token, Json Web 令牌) 是一个开放标准,定义了一种紧凑的、自包含的方式,用于在各方之间以 Json 对象安全地传输信息。此信息可以验证和信任,因为它是数字签名过的。常在 WEB 应用中被用于身份认证。

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

import (
"fmt"
"github.com/dgrijalva/jwt-go"
"time"
)

type SessionClaims struct {
*jwt.StandardClaims
Tag any `json:"tag"` // 用户标识,该结构完全可以自定义,可以任意搭配所需的其他字段信息
}

// NewAuthorization 创建一个新的 JWT,作为用户的 authorization
func NewAuthorization(tag any, secret []byte, expireTime ...int64) (string, error) {
// Create the Claims
iat := time.Now().UTC()
exp := iat.AddDate(0, 0, 7)
claims := SessionClaims{
Tag: tag,
StandardClaims: &jwt.StandardClaims{
ExpiresAt: exp.Unix(),
IssuedAt: iat.Unix(),
},
}
if len(expireTime) != 0 {
claims.StandardClaims.ExpiresAt = expireTime[0] // 手动传入过期时间
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
jwtToken, err := token.SignedString(secret)
if err != nil {
return "", err
}
return jwtToken, nil
}

// ParseAuthorization 解析 authorization JWT
func ParseAuthorization(jwtToken string, secret []byte) (*SessionClaims, error) {
var uploadSessionClaims SessionClaims
parsed, err := jwt.ParseWithClaims(
jwtToken,
&uploadSessionClaims,
func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return secret, nil
},
)
if err != nil {
return nil, err
}

_, ok := parsed.Claims.(*SessionClaims)
if !ok || !parsed.Valid {
return nil, err
}

return &uploadSessionClaims, nil
}

goroutine

在 Go 语言中,可以使用 go 关键字轻松启动一个 goroutine。然而,通过这种方式启动的协程如果内部逻辑存在漏洞,并且未捕获运行异常导致 panic,可能会影响整个应用程序,使其退出。

Go 作为一门服务端语言,常见的应用场景是提供各种服务,因此应用程序运行时的异常退出是我们必须极力避免的。目前主流的 web 框架通常都带有 panic 捕获的中间件,这样在处理 HTTP 请求的协程中出现 panic 时,不会导致整个应用程序崩溃。然而,如果在处理请求的协程中再次启动新的协程,这些新协程的 panic 是无法被捕获的。

因此,有必要对 go 关键字进行封装,增加 panic 捕获功能,确保协程异常不会影响整个应用程序。以下这个包正是为此而设计的。

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

import (
"fmt"
"log"
"runtime"
"strings"
)

func Go(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic info: [ %s ]-[ %v ] \n", identifyPanic(), r)
}
}()

f()
}()
}

// identifyPanic 获取 panic 发生的地方
func identifyPanic() string {
var name, file string
var line int
var pc [16]uintptr

n := runtime.Callers(3, pc[:])
for _, pc := range pc[:n] {
fn := runtime.FuncForPC(pc)
if fn == nil {
continue
}
file, line = fn.FileLine(pc)
name = fn.Name()
if !strings.HasPrefix(name, "runtime.") {
break
}
}

switch {
case name != "":
return fmt.Sprintf("%v:%v", name, line)
case file != "":
return fmt.Sprintf("%v:%v", file, line)
}

return fmt.Sprintf("pc:%x", pc)
}

随机字符串

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

import (
"math/rand"
"strings"
"time"
)

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // 字母集
const numberBytes = "0123456789" // 数字集
const letterAndNumberBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // 字母和数字集
const (
letterIdxBits = 6 // 6位二进制可遍历完所有字符
letterIdxMask = 1<<letterIdxBits - 1 // 掩码
letterIdxMax = 63 / letterIdxBits // 63 位二进制数最多可得到多少个随机字符

numberIdxBits = 4 // 4位二进制可遍历完所有数字
numberIdxMask = 1<<numberIdxBits - 1 // 掩码
numberIdxMax = 63 / numberIdxBits // 63 位二进制数最多可得到多少个随机字符
)

var src = rand.NewSource(time.Now().UnixNano())

// RandString 返回一个 length=n 的随机字符串
func RandString(n int) string {
sb := strings.Builder{}
sb.Grow(n)
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
aLen := len(letterBytes)
if idx := int(cache & letterIdxMask); idx < aLen {
sb.WriteByte(letterBytes[idx])
i--
}
cache >>= letterIdxBits // 随机数右移,重复使用,减少随机数生成,提高效率
remain--
}
return sb.String()
}

// RandStringWithNumber 返回一个 length=n 的随机字符串
func RandStringWithNumber(n int) string {
sb := strings.Builder{}
sb.Grow(n)
arrLen := len(letterAndNumberBytes)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < arrLen {
sb.WriteByte(letterAndNumberBytes[idx])
i--
}
cache >>= letterIdxBits
remain--
}
return sb.String()
}

// RandNumberString 返回一个 length=n 的数字字符串
func RandNumberString(n int) string {
sb := strings.Builder{}
sb.Grow(n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), numberIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), numberIdxMax
}
if idx := int(cache & numberIdxMask); idx < len(numberBytes) {
sb.WriteByte(numberBytes[idx])
i--
}
cache >>= numberIdxBits
remain--
}
return sb.String()
}

// NewUUID 生成一个新的 UUID
func NewUUID() string {
return RandStringWithNumber(24)
}

签名与加密

go 标准库实现了很多加密算法,以下包为这些库的二次封装,简化调用逻辑。

首先是 AES 加密,此处使用的是 PKCS#7 填充方案:

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
/*
Package crypt 签名、加密包
*/
package crypt

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
)

func pKCS7Padding(text []byte, blockSize int) []byte {
// 计算待填充的长度
padding := blockSize - len(text)%blockSize
var paddingText []byte
paddingText = bytes.Repeat([]byte{byte(padding)}, padding)
return append(text, paddingText...)
}

func pKCS7UnPadding(text []byte) []byte {
length := len(text)
unPadding := int(text[length-1])
return text[:(length - unPadding)]
}

func AesCBCEncrypt(text []byte, key []byte, iv []byte) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
// 填充
padText := pKCS7Padding(text, block.BlockSize())
blockMode := cipher.NewCBCEncrypter(block, iv)
// 加密
result := make([]byte, len(padText))
blockMode.CryptBlocks(result, padText)
// 返回密文
return base64Encode(result), nil
}

func AesCBCDecrypt(ciphertext string, key []byte, iv []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// 新建一个解密器
blockMode := cipher.NewCBCDecrypter(block, iv)
// 解密密文
text, err := base64Decode(ciphertext)
if err != nil {
return nil, err
}
plaintext := make([]byte, len(text))
blockMode.CryptBlocks(plaintext, text)
// 去掉填充
plaintext = pKCS7UnPadding(plaintext)
return plaintext, nil
}

func base64Encode(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}

func base64Decode(data string) ([]byte, error) {
return base64.StdEncoding.DecodeString(data)
}

其次是 RSA 加密,使用的是 PKCS#8 格式的秘钥文件和 PKCS#1 V1.5 版本的填充方案:

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

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"os"
)

// RsaEncrypt 使用指定的公钥加密数据,返回密文的 base64 编码
func RsaEncrypt(pubKey *rsa.PublicKey, data []byte) (string, error) {
// 使用公钥加密,使用 PKCS#1 V1.5 填充方案
ciphertext, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, data)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}

// RsaDecrypt 使用指定的私钥解密经过 base64 编码的密文,返回原始数据字节切片
func RsaDecrypt(priKey *rsa.PrivateKey, base64Ciphertext string) ([]byte, error) {
ciphertext, err := base64.StdEncoding.DecodeString(base64Ciphertext)
if err != nil {
return nil, err
}
return rsa.DecryptPKCS1v15(rand.Reader, priKey, ciphertext)
}

// LoadPrivateKeyByPemFile 从 pem 文件(PKCS#8格式)中加载私钥
func LoadPrivateKeyByPemFile(path string) (*rsa.PrivateKey, error) {
privateKeyBytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(privateKeyBytes)
if block == nil {
return nil, errors.New("decode pem file failed")
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return key.(*rsa.PrivateKey), nil
}

// LoadPublicKeyByPemFile 从 pem 文件中加载公钥
func LoadPublicKeyByPemFile(path string) (*rsa.PublicKey, error) {
publicKeyBytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(publicKeyBytes)
if block == nil {
return nil, errors.New("decode pem file failed")
}
publicKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
return publicKeyInterface.(*rsa.PublicKey), nil
}

哈希:

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

import (
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
)

func SHA256Encrypt(s string) string {
hash := sha256.Sum256([]byte(s))
return hex.EncodeToString(hash[:])
}

func SHA1Encrypt(s string) string {
hash := sha1.Sum([]byte(s))
return hex.EncodeToString(hash[:])
}

func HmacSha1Encode(key, data []byte) string {
mac := hmac.New(sha1.New, key)
mac.Write(data)
return hex.EncodeToString(mac.Sum(nil))
}

func MD5Encrypt(s string) string {
m := md5.New()
m.Write([]byte(s))
return hex.EncodeToString(m.Sum(nil))
}

字符串和 []byte 转换

高效完成 string 和 []byte 之间的转换,避免值拷贝。(go1.20+适用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package conv

import (
"unsafe"
)

// StringToBytes 将 string 转换成 []byte。
// Tips: 返回值不可写!
func StringToBytes(s string) (b []byte) {
strPtr := unsafe.StringData(s)
return unsafe.Slice(strPtr, len(s))
}

// BytesToString 将 []byte 转换成 string
func BytesToString(b []byte) string {
return unsafe.String(&b[0], len(b))
}

set

用 map 实现 set 结构。

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

import (
"sync"
)

type Set[T comparable] struct {
m map[T]struct{} `json:"m"`
mt sync.Mutex
}

func New[T comparable]() *Set[T] {
return &Set[T]{
m: make(map[T]struct{}),
}
}

func (s *Set[T]) Add(v T) {
if s == nil {
return
}
s.mt.Lock()
defer s.mt.Unlock()
s.m[v] = struct{}{}
}

func (s *Set[T]) Del(v T) {
if s == nil {
return
}
s.mt.Lock()
defer s.mt.Unlock()
delete(s.m, v)
}

func (s *Set[T]) Has(v T) (exist bool) {
if s == nil {
return false
}
s.mt.Lock()
defer s.mt.Unlock()
_, exist = s.m[v]
return
}

func (s *Set[T]) Length() int {
if s == nil {
return 0
}
return len(s.m)
}

func (s *Set[T]) List() []T {
if s == nil {
return nil
}
res := make([]T, 0, len(s.m))
for v := range s.m {
res = append(res, v)
}
return res
}

func (s *Set[T]) Empty() {
if s == nil {
return
}
s.m = map[T]struct{}{}
return
}

循环队列

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

import (
"github.com/cockroachdb/errors"
"sync"
)

type CircularQueue[T any] struct {
queue []T
rdIdx int
wrIdx int
mt sync.Mutex
}

// NewCircular 创建一个循环队列,队满自动覆盖旧元素
func NewCircular[T any](len uint64) Queue[T] {
return &CircularQueue[T]{
queue: make([]T, len+1),
}
}

func (cq *CircularQueue[T]) Push(val T) {
cq.mt.Lock()
defer cq.mt.Unlock()
if cq.isFull() {
cq.rdIdx++
}
cq.queue[cq.wrIdx] = val
cq.wrIdx = (cq.wrIdx + 1) % len(cq.queue)
}

func (cq *CircularQueue[T]) Pop() (T, error) {
cq.mt.Lock()
defer cq.mt.Unlock()
if cq.isEmpty() {
return *new(T), errors.New("queue is empty")
}
val := cq.queue[cq.rdIdx]
cq.rdIdx = (cq.rdIdx + 1) % len(cq.queue)
return val, nil
}

func (cq *CircularQueue[T]) isEmpty() bool {
return cq.rdIdx == cq.wrIdx
}

func (cq *CircularQueue[T]) isFull() bool {
return (cq.wrIdx+1)%len(cq.queue) == cq.rdIdx
}

func (cq *CircularQueue[T]) List() []T {
cq.mt.Lock()
defer cq.mt.Unlock()
if cq.isEmpty() {
return []T{}
}
if cq.rdIdx < cq.wrIdx {
res := make([]T, 0, cq.wrIdx-cq.rdIdx)
for i := cq.rdIdx; i < cq.wrIdx; i++ {
res = append(res, cq.queue[i])
}
return res
}
res := make([]T, 0, cq.wrIdx+cap(cq.queue)-cq.rdIdx)
for i := cq.rdIdx; i < cap(cq.queue); i++ {
res = append(res, cq.queue[i])
}
for i := 0; i < cq.wrIdx; i++ {
res = append(res, cq.queue[i])
}
return res
}

结语

以上是一些我在开发过程中常用的轮子,后续也会持续进行更新。也欢迎大家分享自己写的或经常用的轮子,让我们共同进步!