跳到主要内容

misc - 实用工具库

misc 是一个提供常用工具函数和类型的实用工具库,包含密码哈希、函数组合、MIME 类型处理、模板插值、泛型工具、数学函数等功能。最低支持 Go 1.21.0 版本。

安装

go get -u go-slim.dev/misc

快速开始

package main

import "go-slim.dev/misc"

func main() {
// 密码哈希
hash, _ := misc.PasswordHash("mypassword")
ok := misc.PasswordVerify("mypassword", hash)

// 模板替换
result, _ := misc.Substitute("Hello {name}!", map[string]any{"name": "World"})

// 泛型工具
value := misc.Coalesce("", "default", "fallback") // "default"

// 数学函数
min, max := misc.MinMax(5, 3) // (3, 5)
}

核心功能

1. 密码哈希和摘要

密码哈希(Bcrypt)

使用 bcrypt 算法进行安全的密码哈希:

import "go-slim.dev/misc"

// 生成密码哈希
hash, err := misc.PasswordHash("mypassword")
if err != nil {
// 处理错误
}

// 验证密码
ok := misc.PasswordVerify("mypassword", hash)
if ok {
// 密码正确
} else {
// 密码错误
}

特点

  • 使用 bcrypt.DefaultCost(当前为 10)
  • 自动生成盐值
  • 防止彩虹表攻击
  • 计算成本可调整

快速哈希函数

用于数据完整性校验和非安全场景:

// MD5(不推荐用于安全场景)
md5Hash := misc.MD5("hello world")
// 返回大写十六进制: "5EB63BBBE01EEED093CB22BB8F5ACDC3"

// SHA-1
sha1Hash := misc.Sha1("hello world")
// 返回小写十六进制: "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"

// SHA-256(推荐)
sha256Hash := misc.Sha256("hello world")
// 返回小写十六进制: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"

使用场景

  • 文件完整性校验
  • 缓存键生成
  • 数据指纹
  • ETag 生成

注意:MD5 和 SHA-1 不应用于安全敏感场景(如密码存储),请使用 SHA-256 或 bcrypt。

2. 函数组合

Call - 顺序执行函数

按顺序执行多个函数,遇到错误立即返回:

import "go-slim.dev/misc"

err := misc.Call(
func() error {
fmt.Println("步骤 1")
return nil
},
func() error {
fmt.Println("步骤 2")
return nil
},
func() error {
fmt.Println("步骤 3")
return errors.New("失败")
},
func() error {
fmt.Println("步骤 4") // 不会执行
return nil
},
)
// 输出: 步骤 1, 步骤 2, 步骤 3
// err 为 "失败"

CallG - 带参数的顺序执行

执行多个接收相同参数的函数:

type User struct {
Name string
Age int
}

user := &User{Name: "Alice", Age: 30}

err := misc.CallG(user,
func(u *User) error {
fmt.Printf("验证用户: %s\n", u.Name)
return nil
},
func(u *User) error {
fmt.Printf("保存用户: %s\n", u.Name)
return db.Save(u)
},
func(u *User) error {
fmt.Printf("发送欢迎邮件给: %s\n", u.Name)
return mailer.Send(u)
},
)

Wrap 和 WrapG - 函数包装

将多个函数组合成一个可复用的函数:

// Wrap - 无参数
initFunc := misc.Wrap(
initDatabase,
initCache,
initLogger,
)

// 多次调用
if err := initFunc(); err != nil {
log.Fatal(err)
}

// WrapG - 带参数
processUser := misc.WrapG(
validateUser,
saveUser,
notifyUser,
)

// 多次使用
for _, user := range users {
if err := processUser(user); err != nil {
log.Printf("处理用户失败: %v", err)
}
}

3. 模板插值

Substitute - 简单模板替换

使用 {} 作为占位符:

// 基本使用
result, err := misc.Substitute("Hello {name}!", map[string]any{
"name": "World",
})
// result: "Hello World!"

// 多个占位符
result, _ := misc.Substitute(
"用户 {name} 的年龄是 {age}",
map[string]any{
"name": "Alice",
"age": 30,
},
)
// result: "用户 Alice 的年龄是 30"

// 未找到的占位符保持原样
result, _ := misc.Substitute(
"Hello {name}, {missing}",
map[string]any{"name": "World"},
)
// result: "Hello World, {missing}"

Interpolate - 自定义分隔符

使用自定义的起始和结束标记:

// 使用 {{ }} 分隔符
result, _ := misc.Interpolate(
"/user/{{ID}}/posts/{{PostID}}",
"{{", "}}",
map[string]any{
"ID": 123,
"PostID": 456,
},
)
// result: "/user/123/posts/456"

// 使用 [[ ]] 分隔符
result, _ := misc.Interpolate(
"SELECT * FROM users WHERE id = [[id]]",
"[[", "]]",
map[string]any{"id": 42},
)
// result: "SELECT * FROM users WHERE id = 42"

// 通配符 "*" 作为默认值
result, _ := misc.Interpolate(
"{greeting} {name}!",
"{", "}",
map[string]any{
"name": "Alice",
"*": "未知", // 默认值
},
)
// result: "未知 Alice!"

Tmpl - 高级模板处理

使用自定义的 TagFunc 处理占位符:

import (
"io"
"strings"
"go-slim.dev/misc"
)

var output strings.Builder

// 自定义标签处理函数
tagFunc := func(w io.Writer, tag string) (int, error) {
switch tag {
case "date":
return w.Write([]byte(time.Now().Format("2006-01-02")))
case "time":
return w.Write([]byte(time.Now().Format("15:04:05")))
case "upper":
// 可以访问上下文或执行复杂逻辑
return w.Write([]byte("HELLO"))
default:
return w.Write([]byte("未知标签"))
}
}

n, err := misc.Tmpl(
"日期: {date}, 时间: {time}, 消息: {upper}",
"{", "}",
&output,
tagFunc,
)

fmt.Println(output.String())
// 输出: 日期: 2024-01-15, 时间: 14:30:45, 消息: HELLO

4. 泛型工具函数

Zero - 获取零值

返回任意类型的零值:

import "go-slim.dev/misc"

zeroInt := misc.Zero[int]() // 0
zeroStr := misc.Zero[string]() // ""
zeroBool := misc.Zero[bool]() // false
zeroPtr := misc.Zero[*int]() // nil
zeroSlice := misc.Zero[[]int]() // nil
zeroMap := misc.Zero[map[string]int]() // nil

// 在泛型函数中使用
func GetOrDefault[T any](m map[string]T, key string) T {
if v, ok := m[key]; ok {
return v
}
return misc.Zero[T]()
}

Ptr - 创建指针

返回值的指针副本:

// 基本类型
intPtr := misc.Ptr(42) // *int
strPtr := misc.Ptr("hello") // *string
boolPtr := misc.Ptr(true) // *bool

// 在结构体字面量中使用
type Config struct {
Port *int
Host *string
Enabled *bool
}

cfg := Config{
Port: misc.Ptr(8080),
Host: misc.Ptr("localhost"),
Enabled: misc.Ptr(true),
}

// API 调用场景
updateUser(&User{
ID: 123,
Name: misc.Ptr("Alice"), // 只更新 Name 字段
})

Nil - 获取类型化 nil 指针

返回特定类型的 nil 指针:

var intPtr *int = misc.Nil[int]()
var strPtr *string = misc.Nil[string]()
var slicePtr *[]int = misc.Nil[[]int]()

// 在泛型函数中使用
func MaybeValue[T any](hasValue bool, value T) *T {
if hasValue {
return &value
}
return misc.Nil[T]()
}

IsZero - 零值判断

判断任意值是否为零值(支持深度检查):

misc.IsZero(0)                  // true
misc.IsZero("") // true
misc.IsZero(false) // true
misc.IsZero((*int)(nil)) // true
misc.IsZero([]int(nil)) // true
misc.IsZero(map[string]int(nil)) // true

misc.IsZero(42) // false
misc.IsZero("hello") // false
misc.IsZero(true) // false
misc.IsZero(&User{}) // false(指针不为 nil)

// 指针会递归解引用
ptr := new(int) // *int 指向 0
misc.IsZero(ptr) // true(解引用后值为 0)

*ptr = 42
misc.IsZero(ptr) // false(解引用后值为 42)

IsNil - nil 判断

判断值是否为 nil(适用于引用类型):

misc.IsNil(nil)                    // true
misc.IsNil((*int)(nil)) // true
misc.IsNil([]int(nil)) // true
misc.IsNil(map[string]int(nil)) // true
misc.IsNil((chan int)(nil)) // true
misc.IsNil((func())(nil)) // true

misc.IsNil(0) // false
misc.IsNil("") // false
misc.IsNil(make([]int, 0)) // false(空切片,非 nil)
misc.IsNil(make(map[string]int)) // false(空 map,非 nil)

// 接口值
var err error = nil
misc.IsNil(err) // true

err = fmt.Errorf("error")
misc.IsNil(err) // false

Coalesce - 取首个非零值

返回参数列表中第一个非零值:

// 字符串默认值
name := misc.Coalesce("", "default", "fallback")
// name = "default"

// 整数默认值
port := misc.Coalesce(0, 8080, 3000)
// port = 8080

// 配置优先级:命令行 > 环境变量 > 配置文件 > 默认值
port := misc.Coalesce(
cliPort, // 命令行参数
envPort, // 环境变量
configPort, // 配置文件
8080, // 默认值
)

// 用户输入回退
username := misc.Coalesce(
userInput,
savedUsername,
"guest",
)

// 全部为零值时返回零值
result := misc.Coalesce[int]() // 0
result := misc.Coalesce("", "", "") // ""

5. 数学函数

MinMax - 同时获取最小和最大值

返回两个值的最小值和最大值:

import "go-slim.dev/misc"

// 整数
min, max := misc.MinMax(5, 3)
// min = 3, max = 5

// 浮点数
min, max := misc.MinMax(1.5, 2.7)
// min = 1.5, max = 2.7

// 字符串(字典序)
min, max := misc.MinMax("banana", "apple")
// min = "apple", max = "banana"

// 相等的值
min, max := misc.MinMax(10, 10)
// min = 10, max = 10

// 实际应用
func normalizeRange(a, b int) (start, end int) {
return misc.MinMax(a, b)
}

start, end := normalizeRange(100, 50)
// start = 50, end = 100

Clamp - 限制值在范围内

将值限制在 [min, max] 范围内:

// 基本用法
result := misc.Clamp(15, 10, 20) // 15(在范围内)
result := misc.Clamp(5, 10, 20) // 10(低于最小值)
result := misc.Clamp(25, 10, 20) // 20(高于最大值)

// 自动处理边界反转
result := misc.Clamp(15, 20, 10) // 15(自动将边界调整为 10, 20)

// 浮点数
opacity := misc.Clamp(1.5, 0.0, 1.0) // 1.0

// 实际应用:限制百分比
func setVolume(vol int) int {
return misc.Clamp(vol, 0, 100)
}

// 限制颜色值
func normalizeColor(c int) int {
return misc.Clamp(c, 0, 255)
}

// 限制坐标
x := misc.Clamp(mouseX, 0, screenWidth)
y := misc.Clamp(mouseY, 0, screenHeight)

6. 零拷贝转换

使用 unsafe 实现零拷贝的字符串和字节切片转换:

import "go-slim.dev/misc"

// 字节切片转字符串
bytes := []byte("hello world")
str := misc.BytesToString(bytes)
// 零拷贝转换,共享底层数据

// 字符串转字节切片
str := "hello world"
bytes := misc.StringToBytes(str)
// 零拷贝转换,共享底层数据

⚠️ 重要警告

  1. 不要修改源数据:转换后的值共享底层内存,修改会导致未定义行为
  2. 不要修改结果StringToBytes 返回的切片是只读的
  3. 生命周期:确保源数据在使用结果期间保持有效

正确使用

// ✅ 正确:只读使用
bytes := []byte("hello")
str := misc.BytesToString(bytes)
fmt.Println(str) // 安全

// ✅ 正确:临时使用
if misc.BytesToString(data) == "expected" {
// 安全
}

// ❌ 错误:修改源数据
bytes := []byte("hello")
str := misc.BytesToString(bytes)
bytes[0] = 'H' // 未定义行为!str 可能会改变

// ❌ 错误:修改结果
str := "hello"
bytes := misc.StringToBytes(str)
bytes[0] = 'H' // 未定义行为!可能崩溃或损坏内存

使用场景

// 性能敏感的字符串比较
func fastCompare(a []byte, b string) bool {
return misc.BytesToString(a) == b
}

// HTTP 头部解析(只读)
func parseHeader(line []byte) (key, value string) {
parts := bytes.SplitN(line, []byte(":"), 2)
return misc.BytesToString(parts[0]),
misc.BytesToString(bytes.TrimSpace(parts[1]))
}

// JSON 解析优化
func parseField(data []byte) string {
// data 从池中获取,使用后立即返回池
return misc.BytesToString(data) // 危险!data 可能被重用
}

// 更安全的做法
func parseFieldSafe(data []byte) string {
return string(data) // 标准转换,创建副本
}

7. MIME 类型处理

已废弃:推荐使用 github.com/h2non/filetype 库。

// ExtensionByType - 根据 MIME 类型获取扩展名
ext := misc.ExtensionByType("image/png") // ".png"
ext := misc.ExtensionByType("video/mp4") // ".mp4"

// TypeByExtension - 根据扩展名获取 MIME 类型
mimeType := misc.TypeByExtension(".jpg") // "image/jpeg"
mimeType := misc.TypeByExtension("png") // "image/png"(可省略点)

// CharsetByType - 获取字符集(功能有限)
charset := misc.CharsetByType("text/html") // "charset=utf-8"
charset := misc.CharsetByType("image/png") // ""

8. 栈跟踪

已废弃:推荐使用 runtime.Stackdebug.Stack

// Stack - 获取带源代码行的栈跟踪
trace := misc.Stack(0) // 0 表示从调用者开始
fmt.Println(trace)

使用场景

1. Web 应用中的用户认证

import (
"go-slim.dev/misc"
"go-slim.dev/slim"
)

type User struct {
Username string
Password string
}

// 注册用户
func registerUser(c slim.Context) error {
var user User
if err := c.Bind(&user); err != nil {
return err
}

// 生成密码哈希
hash, err := misc.PasswordHash(user.Password)
if err != nil {
return c.String(500, "密码加密失败")
}

// 保存到数据库
user.Password = hash
if err := db.Save(&user); err != nil {
return c.String(500, "保存失败")
}

return c.String(200, "注册成功")
}

// 用户登录
func loginUser(c slim.Context) error {
var input User
if err := c.Bind(&input); err != nil {
return err
}

// 从数据库获取用户
var user User
if err := db.FindByUsername(input.Username, &user); err != nil {
return c.String(401, "用户名或密码错误")
}

// 验证密码
if !misc.PasswordVerify(input.Password, user.Password) {
return c.String(401, "用户名或密码错误")
}

// 生成 token 并返回
token := generateToken(user)
return c.JSON(200, map[string]string{"token": token})
}

2. 配置管理

import (
"os"
"strconv"
"go-slim.dev/misc"
)

type Config struct {
Host string
Port int
Database string
Debug bool
}

func loadConfig() *Config {
// 使用 Coalesce 实现配置优先级
// 命令行 > 环境变量 > 默认值

portStr := misc.Coalesce(
os.Getenv("APP_PORT"),
"8080",
)
port, _ := strconv.Atoi(portStr)

return &Config{
Host: misc.Coalesce(
os.Getenv("APP_HOST"),
"localhost",
),
Port: port,
Database: misc.Coalesce(
os.Getenv("DATABASE_URL"),
"postgres://localhost/myapp",
),
Debug: os.Getenv("DEBUG") == "true",
}
}

3. URL 路由模板

import "go-slim.dev/misc"

type Router struct {
baseURL string
}

func (r *Router) UserProfile(userID int) string {
url, _ := misc.Substitute(
r.baseURL+"/users/{id}/profile",
map[string]any{"id": userID},
)
return url
}

func (r *Router) UserPosts(userID, page int) string {
url, _ := misc.Interpolate(
r.baseURL+"/users/{{userID}}/posts?page={{page}}",
"{{", "}}",
map[string]any{
"userID": userID,
"page": page,
},
)
return url
}

// 使用
router := &Router{baseURL: "https://api.example.com"}
profileURL := router.UserProfile(123)
// "https://api.example.com/users/123/profile"

postsURL := router.UserPosts(123, 2)
// "https://api.example.com/users/123/posts?page=2"

4. 数据验证流程

import "go-slim.dev/misc"

type Order struct {
ID int
UserID int
Amount float64
Status string
}

func validateOrder(order *Order) error {
return misc.Call(
func() error {
if order.UserID <= 0 {
return errors.New("无效的用户 ID")
}
return nil
},
func() error {
if order.Amount <= 0 {
return errors.New("金额必须大于 0")
}
return nil
},
func() error {
if order.Status == "" {
return errors.New("状态不能为空")
}
return nil
},
)
}

func processOrder(order *Order) error {
return misc.CallG(order,
validateOrder,
saveOrder,
sendConfirmation,
updateInventory,
)
}

5. 邮件模板

import "go-slim.dev/misc"

type EmailTemplate struct {
template string
}

func (t *EmailTemplate) Render(data map[string]any) (string, error) {
return misc.Substitute(t.template, data)
}

// 欢迎邮件
welcomeEmail := &EmailTemplate{
template: `
亲爱的 {name},

欢迎加入 {company}!

您的账户已创建成功:
- 用户名: {username}
- 邮箱: {email}

请访问 {url} 完成激活。

祝好,
{company} 团队
`,
}

content, _ := welcomeEmail.Render(map[string]any{
"name": "Alice",
"company": "TechCorp",
"username": "alice123",
"email": "alice@example.com",
"url": "https://example.com/activate",
})

6. 输入范围限制

import "go-slim.dev/misc"

// 图像处理
type Image struct {
Width int
Height int
}

func (img *Image) SetBrightness(value float64) {
// 限制亮度在 0.0 到 2.0 之间
img.brightness = misc.Clamp(value, 0.0, 2.0)
}

func (img *Image) SetPixel(x, y int, color RGB) {
// 限制坐标在图像范围内
x = misc.Clamp(x, 0, img.Width-1)
y = misc.Clamp(y, 0, img.Height-1)

// 限制颜色值在 0-255 范围内
r := misc.Clamp(color.R, 0, 255)
g := misc.Clamp(color.G, 0, 255)
b := misc.Clamp(color.B, 0, 255)

img.setPixelUnsafe(x, y, RGB{r, g, b})
}

// 音量控制
type AudioPlayer struct {
volume int
}

func (p *AudioPlayer) SetVolume(vol int) {
p.volume = misc.Clamp(vol, 0, 100)
}

func (p *AudioPlayer) IncreaseVolume(delta int) {
p.volume = misc.Clamp(p.volume+delta, 0, 100)
}

7. 文件完整性校验

import (
"io"
"os"
"go-slim.dev/misc"
)

func calculateFileHash(filename string) (string, error) {
data, err := os.ReadFile(filename)
if err != nil {
return "", err
}

// 使用 SHA-256 计算文件哈希
return misc.Sha256(string(data)), nil
}

func verifyFileIntegrity(filename, expectedHash string) (bool, error) {
actualHash, err := calculateFileHash(filename)
if err != nil {
return false, err
}

return actualHash == expectedHash, nil
}

// 使用示例
ok, err := verifyFileIntegrity("download.zip", "b94d27b99...")
if !ok {
log.Fatal("文件已损坏或被篡改")
}

API 参考

密码和摘要

// 密码哈希(bcrypt)
func PasswordHash(password string) (string, error)
func PasswordVerify(password, hash string) bool

// 快速哈希
func MD5(str string) string // 返回大写十六进制
func Sha1(str string) string // 返回小写十六进制
func Sha256(str string) string // 返回小写十六进制

函数组合

func Call(fns ...func() error) error
func CallG[T any](val T, fns ...func(T) error) error
func Wrap(fns ...func() error) func() error
func WrapG[T any](fns ...func(T) error) func(val T) error

模板插值

func Substitute(template string, data map[string]any) (string, error)
func Interpolate(template, startTag, endTag string, data map[string]any) (string, error)
func Tmpl(template, startTag, endTag string, w io.Writer, f TagFunc) (int64, error)

type TagFunc func(w io.Writer, tag string) (int, error)

泛型工具

func Zero[T any]() T
func Ptr[T any](x T) *T
func Nil[T any]() *T
func IsZero[T any](v T) bool
func IsNil(x any) bool
func Coalesce[T any](values ...T) T

数学函数

func MinMax[T cmp.Ordered](a, b T) (T, T)
func Clamp[T cmp.Ordered](val, minT, maxT T) T

零拷贝转换

func BytesToString(b []byte) string
func StringToBytes(s string) []byte

MIME 类型(已废弃)

func ExtensionByType(mimeType string) string
func TypeByExtension(ext string) string
func CharsetByType(typ string) string

栈跟踪(已废弃)

func Stack(skip int) string

注意事项

  1. 密码哈希性能

    • bcrypt 故意设计为慢速,以防止暴力破解
    • 不要在性能敏感的代码路径中频繁调用
    • 默认 cost 为 10,每增加 1,耗时翻倍
  2. 哈希函数选择

    • 安全场景(密码、签名):使用 PasswordHash 或 SHA-256
    • 非安全场景(缓存键、ETag):可使用 MD5 或 SHA-1
    • MD5 和 SHA-1 不应用于密码存储
  3. 零拷贝转换风险

    • BytesToStringStringToBytes 使用 unsafe
    • 共享底层内存,修改源数据会导致未定义行为
    • 仅在确定数据不会被修改时使用
    • 性能提升显著,但需要谨慎使用
  4. IsZero vs IsNil

    • IsZero: 检查是否为类型的零值(如 0, "", false, nil)
    • IsNil: 仅检查引用类型是否为 nil
    • IsZero 对指针会递归解引用
  5. Coalesce 行为

    • 返回第一个非零值,不是第一个非 nil 值
    • 空字符串 "" 是零值,会被跳过
    • 空切片 []int{} 不是零值,会被返回
  6. 函数组合顺序

    • CallCallG 按顺序执行函数
    • 遇到第一个错误立即停止
    • 后续函数不会执行
  7. 模板插值安全

    • Substitute 不进行 HTML 转义
    • 如需在 HTML 中使用,需自行转义
    • 避免将不可信输入直接插入模板
  8. Clamp 边界处理

    • 自动处理 min > max 的情况(会交换边界)
    • 使用前无需手动检查边界顺序

性能建议

  1. 密码哈希:异步处理,避免阻塞主流程
  2. 零拷贝转换:仅在性能关键路径使用,注意安全性
  3. 函数组合:避免在循环中使用 Wrap,应在循环外创建
  4. 模板插值:频繁使用时考虑缓存结果或使用 text/template

相关链接