Skip to content

常量与错误

库定义的常量、错误类型、哨兵错误和预定义变量。

安全限制常量

默认限制

go
const (
    // DefaultMaxFileSize - 单文件最大字节数
    DefaultMaxFileSize int64 = 2 * 1024 * 1024  // 2 MB

    // DefaultMaxLineLength - 单行最大长度
    DefaultMaxLineLength int = 1024  // 1 KB

    // DefaultMaxKeyLength - 键名最大长度
    DefaultMaxKeyLength int = 64

    // DefaultMaxValueLength - 值最大长度
    DefaultMaxValueLength int = 4096  // 4 KB

    // DefaultMaxVariables - 每文件最大变量数
    DefaultMaxVariables int = 500

    // DefaultMaxExpansionDepth - 变量展开最大深度
    DefaultMaxExpansionDepth int = 5
)

硬性上限

注意

以下为库内部硬性上限(未导出),用于 Config.Validate() 内部检查。用户无法直接引用这些常量,但 cfg.Validate() 会自动检查配置是否超过这些限制。

常量说明
HardMaxFileSize100 MB文件大小硬性上限
HardMaxLineLength64 KB行长度硬性上限
HardMaxKeyLength1024键长度硬性上限
HardMaxValueLength1 MB值长度硬性上限
HardMaxVariables10000变量数硬性上限
HardMaxExpansionDepth20展开深度硬性上限

配置验证会检查是否超过硬性限制:

go
cfg := env.DefaultConfig()
cfg.MaxFileSize = 200 * 1024 * 1024  // 超过 100MB 上限

if err := cfg.Validate(); err != nil {
    // 返回错误:MaxFileSize exceeds hard limit
}

哨兵错误

注意

以下哨兵均为预定义符号,但当前实现中部分场景不通过 errors.Is 匹配这些哨兵:禁止键返回 *SecurityError(用 errors.Is(err, ErrSecurityViolation) 匹配),键格式非法与缺少必需键返回 *ValidationError(用 errors.As 提取)。详见各错误类型章节。

文件错误

go
var ErrFileNotFound = errors.New("file not found")
var ErrFileTooLarge = errors.New("file exceeds maximum size limit")

检查方式:

go
err := loader.LoadFiles(".env")
if errors.Is(err, env.ErrFileNotFound) {
    // 文件不存在
}
if errors.Is(err, env.ErrFileTooLarge) {
    // 文件过大
}

解析错误

go
var ErrLineTooLong = errors.New("line exceeds maximum length limit")
var ErrInvalidKey = errors.New("invalid key format")
var ErrDuplicateKey = errors.New("duplicate key encountered")

安全错误

go
var ErrForbiddenKey = errors.New("key is forbidden for security reasons")
var ErrSecurityViolation = errors.New("security policy violation")
var ErrInvalidValue = errors.New("invalid value content")

检查禁止键:

go
err := loader.Set("PATH", "value")
if errors.Is(err, env.ErrSecurityViolation) {
    // 尝试设置禁止键返回 *SecurityError
}

展开错误

go
var ErrExpansionDepth = errors.New("variable expansion depth exceeded")

限制错误

go
var ErrMaxVariables = errors.New("maximum number of variables exceeded")

状态错误

go
var ErrClosed = errors.New("loader has been closed")
var ErrInvalidConfig = errors.New("invalid configuration")
var ErrAlreadyInitialized = errors.New("default loader already initialized")
var ErrNotInitialized = errors.New("default loader not initialized; call Load() first")
var ErrMissingRequired = errors.New("required key is missing")

检查方式:

go
// 检查加载器是否已关闭
if errors.Is(err, env.ErrClosed) {
    // 加载器已关闭
}

// 检查默认加载器是否已初始化
if errors.Is(err, env.ErrAlreadyInitialized) {
    // 默认加载器已存在,无法重复调用 Load()
}

// 检查默认加载器是否未初始化
if errors.Is(err, env.ErrNotInitialized) {
    // 需要先调用 env.Load() 或 env.LoadWithConfig()
}

// 检查必需键是否缺失(实际返回 *ValidationError{Rule:"required"})
var valErr *env.ValidationError
if errors.As(err, &valErr) && valErr.Rule == "required" {
    // 缺少必需键
}

适配器错误

go
var ErrValidateRequiredUnsupported = errors.New(
    "custom validator does not implement ValidateRequired; " +
    "implement Validator interface for required key validation",
)

当自定义验证器仅实现 KeyValidator 接口而未实现完整 Validator 接口时,调用 ValidateRequired 会返回此错误。

检查方式:

go
if errors.Is(err, env.ErrValidateRequiredUnsupported) {
    // 自定义验证器不支持必需键验证
    // 需要实现完整的 Validator 接口
}

解决方法

实现 Validator 接口(包含 ValidateKey、ValidateValue、ValidateRequired 三个方法)而非仅实现 KeyValidator

错误类型

ParseError

解析错误,包含位置信息:

go
type ParseError struct {
    File    string  // 文件名
    Line    int     // 行号
    Content string  // 错误内容(已掩码)
    Err     error   // 原始错误
}

使用示例:

go
err := loader.LoadFiles(".env")
var parseErr *env.ParseError
if errors.As(err, &parseErr) {
    fmt.Printf("解析错误 %s:%d: %v\n",
        parseErr.File, parseErr.Line, parseErr.Err)
}

ValidationError

验证错误:

go
type ValidationError struct {
    Field   string  // 字段名
    Value   string  // 值(已掩码)
    Rule    string  // 规则
    Message string  // 消息
}

SecurityError

安全错误:

go
type SecurityError struct {
    Action  string  // 操作
    Reason  string  // 原因
    Key     string  // 键名(已掩码)
    Details string  // 额外详情
}

使用示例:

go
var secErr *env.SecurityError
if errors.As(err, &secErr) {
    fmt.Printf("安全错误: %s - %s\n", secErr.Action, secErr.Reason)
}

FileError

文件操作错误:

go
type FileError struct {
    Path  string  // 文件路径
    Op    string  // 操作(open, stat, size_check)
    Err   error   // 原始错误
    Size  int64   // 文件大小(Size 检查时)
    Limit int64   // 限制(Size 检查时)
}

使用示例:

go
var fileErr *env.FileError
if errors.As(err, &fileErr) {
    fmt.Printf("文件 %s 大小 %d 超过限制 %d\n",
        fileErr.Path, fileErr.Size, fileErr.Limit)
}

ExpansionError

变量展开错误:

go
type ExpansionError struct {
    Key   string             // 键名
    Depth int                // 当前深度
    Limit int                // 限制
    Chain string             // 展开链(已脱敏)
    Kind  ExpansionErrorKind // 错误原因类别(零值 = 深度/循环)
}

错误分类(Kind 字段):

go
type ExpansionErrorKind int

const (
    // ExpansionDepthKind 表示展开触达递归深度限制或检测到变量循环。
    // 这是零值,因此常见的深度/循环错误无需显式分类。
    // errors.Is(err, ErrExpansionDepth) 可匹配此类错误。
    ExpansionDepthKind ExpansionErrorKind = iota

    // ExpansionRequiredKind 表示必需变量(${VAR:?message})未设置或为空。
    // 这不属于深度溢出,因此不会匹配 ErrExpansionDepth。
    ExpansionRequiredKind
)

errors.Is 行为: *ExpansionError 仅在 Kind != ExpansionRequiredKind 时匹配 ErrExpansionDepth。必需变量错误属于独立的失败模式,不通过 ErrExpansionDepth 匹配。

使用示例:

go
var expErr *env.ExpansionError
if errors.As(err, &expErr) {
    switch expErr.Kind {
    case env.ExpansionDepthKind:
        // 深度溢出或循环:errors.Is(err, env.ErrExpansionDepth) == true
        fmt.Printf("深度 %d/%d,链:%s\n", expErr.Depth, expErr.Limit, expErr.Chain)
    case env.ExpansionRequiredKind:
        // 必需变量未设置:errors.Is(err, env.ErrExpansionDepth) == false
        fmt.Printf("必需变量 %s 未设置\n", expErr.Key)
    }
}

JSONError

JSON 解析错误:

go
type JSONError struct {
    Path    string  // 文件路径
    Message string  // 错误消息
    Err     error   // 原始错误
}

YAMLError

YAML 解析错误:

go
type YAMLError struct {
    Path    string  // 文件路径
    Line    int     // 行号
    Column  int     // 列号
    Message string  // 错误消息
    Err     error   // 原始错误
}

MarshalError

序列化错误:

go
type MarshalError struct {
    Field   string  // 字段名
    Message string  // 错误消息
}

func IsMarshalError(err error) bool  // 检查函数

预定义变量

DefaultForbiddenKeys

内置禁止键列表,防止修改系统关键变量:

注意

defaultForbiddenKeys 为库内部变量(未导出),无法直接通过 env.DefaultForbiddenKeys 访问。以下为内部使用的完整列表,供参考。

类别禁止键
系统路径PATH
动态链接器 (Linux)LD_PRELOAD, LD_PRELOAD_32, LD_PRELOAD_64, LD_LIBRARY_PATH, LD_LIBRARY_PATH_32, LD_LIBRARY_PATH_64, LD_AUDIT, LD_DEBUG
macOSDYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH
WindowsCOMSPEC, PATHEXT, SYSTEMROOT, WINDIR
ShellSHELL, ENV, BASH_ENV, IFS
语言运行时PYTHONPATH, NODE_PATH, PERL5OPT, RUBYLIB

风险说明:

风险类型说明
PATH命令劫持修改命令搜索路径
LD_PRELOAD库注入预加载恶意动态库
LD_LIBRARY_PATH库劫持修改库搜索路径
DYLD_INSERT_LIBRARIES库注入macOS 库注入
COMSPEC命令劫持Windows 命令解释器路径覆盖
PATHEXT命令劫持Windows 可执行文件扩展名篡改
SYSTEMROOT系统破坏Windows 系统根目录篡改
WINDIR系统破坏Windows 目录篡改
PYTHONPATH模块劫持Python 模块搜索路径
IFS解析攻击修改字段分隔符

使用示例:

go
// 设置禁止键返回 *SecurityError
err := loader.Set("PATH", "/malicious/path")
if errors.Is(err, env.ErrSecurityViolation) {
    // 键被禁止
}

// 添加额外的禁止键
cfg := env.DefaultConfig()
cfg.ForbiddenKeys = []string{"MY_SENSITIVE_VAR"}

SensitiveKeyPatterns

敏感键模式列表,用于自动检测敏感配置。键名包含这些模式(不区分大小写)时会被识别为敏感:

注意

sensitiveKeyPatterns 为库内部变量(未导出),通过 IsSensitiveKey() 函数间接访问。以下为主要敏感模式类别,供参考。

主要敏感模式类别:

类别模式示例
认证与授权PASSWORD, SECRET, TOKEN, AUTH, CREDENTIAL, PASSPHRASE, SESSION, COOKIE
API 与密钥API_KEY, APIKEY, ACCESS_KEY, SECRET_KEY, PRIVATE_KEY, PUBLIC_KEY
加密与安全PRIVATE, ENCRYPTION_KEY, ENCRYPT_KEY, DECRYPT_KEY, SIGNING_KEY, SIGN_KEY, VERIFY_KEY
金融与 PIISSN, SOCIAL_SECURITY, CREDIT_CARD, CARD_NUMBER, CVV, CVC, CCV, PAN
加密货币MNEMONIC, SEED, RECOVERY, WALLET, PRIVATE_ADDRESS
数据库CONNECTION_STRING, CONN_STRING, DATABASE_URL, DB_PASSWORD
云服务AWS_SECRET, AZURE_KEY, GCP_KEY, SERVICE_ACCOUNT

匹配规则:

  • 不区分大小写
  • 键名包含任一模式即被识别为敏感

使用示例:

go
// 检查键是否敏感
if env.IsSensitiveKey("DB_PASSWORD") {
    // 使用安全方式处理
    secret := env.GetSecure("DB_PASSWORD")
    if secret != nil {
        defer secret.Release()
    }
}

DefaultKeyPattern

默认键名验证模式:

go
var DefaultKeyPattern *regexp.Regexp = nil

性能优化

nil 值启用快速字节级验证(约 10 倍性能提升)。 默认验证规则:以字母开头,只包含字母、数字、下划线。

自定义模式:

go
import "regexp"

cfg := env.DefaultConfig()
// 只允许大写字母开头
cfg.KeyPattern = regexp.MustCompile(`^[A-Z][A-Z0-9_]{1,63}$`)

安全工具函数

IsSensitiveKey

go
func IsSensitiveKey(key string) bool

检查键名是否匹配敏感模式。

go
if env.IsSensitiveKey("DB_PASSWORD") {
    // 敏感键,使用安全方式处理
    secret := env.GetSecure("DB_PASSWORD")
    defer secret.Release()
}

MaskValue

go
func MaskValue(key, value string) string

根据键的敏感性返回掩码值。

go
// 敏感键 - 返回 [MASKED:N chars] 格式
masked := env.MaskValue("API_KEY", "secret123")
// 返回:[MASKED:9 chars]

// 非敏感键 - 返回原值(超过 20 字符则截断)
masked := env.MaskValue("APP_NAME", "myapp")
// 返回:myapp
masked := env.MaskValue("DESCRIPTION", "this is a very long description text")
// 返回:this is a very lo...

MaskKey

go
func MaskKey(key string) string

掩码键名用于日志。

go
masked := env.MaskKey("DB_PASSWORD")
// 返回:DB***

MaskSensitiveInString

go
func MaskSensitiveInString(s string) string

掩码字符串中的潜在敏感内容。截断超过 50 字符的字符串。

参数:

  • s - 原始字符串

返回:

  • string - 掩码后的字符串
go
// 长字符串会被截断
log := "This is a very long log message that exceeds 50 characters and will be truncated"
clean := env.MaskSensitiveInString(log)
// 返回:"This is a very long log message that exceeds 50..."

// 短字符串保持不变
short := "Short message"
clean := env.MaskSensitiveInString(short)
// 返回:"Short message"

注意

此函数主要用于截断长字符串。如需自动掩码敏感键值对,请使用 SanitizeForLog

SanitizeForLog

go
func SanitizeForLog(s string) string

清理字符串中的敏感键值对信息。自动检测并掩码 key=value 格式中的敏感值。

参数:

  • s - 原始字符串

返回:

  • string - 清理后的字符串

检测的敏感键模式:

  • password=, secret=, token=, auth=, credential=, passphrase=, session=, cookie=
  • api_key=, apikey=, access_key=, secret_key=, private_key=, public_key=
  • encrypt_key=, decrypt_key=, signing_key=
  • ssn=, credit_card=, card_number=, cvv=, cvc=
  • mnemonic=, seed=, recovery=, wallet=
  • connection_string=, database_url=, db_password=
go
// 自动掩码敏感键值对
msg := "Connected with password=secret123 api_key=abc123"
clean := env.SanitizeForLog(msg)
// 返回:"Connected with password=[MASKED] api_key=[MASKED]"

// 非敏感键值对保持不变
msg := "Config loaded: app_name=myapp port=8080"
clean := env.SanitizeForLog(msg)
// 返回:"Config loaded: app_name=myapp port=8080"

使用场景

适用于日志输出、错误消息、调试信息等需要自动过滤敏感键值对的场景。

ClearBytes

go
func ClearBytes(b []byte)

安全清零字节切片。

go
sensitive := []byte("secret-data")
// 使用...
env.ClearBytes(sensitive)
// sensitive 现在全是 0

FileFormat 常量

文件格式类型:

go
type FileFormat int

const (
    FormatAuto  FileFormat = iota  // 自动检测
    FormatEnv                      // .env 格式
    FormatJSON                     // JSON 格式
    FormatYAML                     // YAML 格式
)

使用示例:

go
// 检测格式
format := env.DetectFormat("config.json")  // FormatJSON

// 指定格式序列化
data, _ := env.Marshal(cfg, env.FormatJSON)

// 格式字符串
fmt.Println(format.String())  // "json"

错误检查模式

errors.Is 模式

检查哨兵错误:

go
err := loader.LoadFiles(".env")

switch {
case errors.Is(err, env.ErrFileNotFound):
    // 文件不存在
case errors.Is(err, env.ErrFileTooLarge):
    // 文件过大
case errors.Is(err, env.ErrSecurityViolation):
    // 禁止键
case errors.Is(err, env.ErrClosed):
    // 加载器已关闭
}

errors.As 模式

提取详细错误信息:

go
err := loader.LoadFiles(".env")

var parseErr *env.ParseError
if errors.As(err, &parseErr) {
    fmt.Printf("解析错误在 %s%d\n", parseErr.File, parseErr.Line)
}

var fileErr *env.FileError
if errors.As(err, &fileErr) {
    fmt.Printf("文件 %s 大小 %d 超过限制 %d\n",
        fileErr.Path, fileErr.Size, fileErr.Limit)
}

var secErr *env.SecurityError
if errors.As(err, &secErr) {
    fmt.Printf("安全错误: %s - %s\n", secErr.Action, secErr.Reason)
}

完整错误处理示例

go
package main

import (
    "errors"
    "log"

    "github.com/cybergodev/env"
)

func main() {
    cfg := env.ProductionConfig()
    cfg.FailOnMissingFile = true

    loader, err := env.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer loader.Close()

    err = loader.LoadFiles(".env")
    if err != nil {
        switch {
        case errors.Is(err, env.ErrFileNotFound):
            log.Fatal("配置文件不存在")

        case errors.Is(err, env.ErrFileTooLarge):
            log.Fatal("配置文件过大")

        case errors.Is(err, env.ErrClosed):
            log.Fatal("加载器已关闭")

        default:
            var parseErr *env.ParseError
            if errors.As(err, &parseErr) {
                log.Fatalf("解析错误 %s:%d - %v",
                    parseErr.File, parseErr.Line, parseErr.Err)
            }

            var fileErr *env.FileError
            if errors.As(err, &fileErr) {
                log.Fatalf("文件错误 %s - %v", fileErr.Path, fileErr.Err)
            }

            var secErr *env.SecurityError
            if errors.As(err, &secErr) {
                log.Fatalf("安全错误: %s - %s", secErr.Action, secErr.Reason)
            }

            var jsonErr *env.JSONError
            if errors.As(err, &jsonErr) {
                log.Fatalf("JSON 错误 %s: %s", jsonErr.Path, jsonErr.Message)
            }

            var yamlErr *env.YAMLError
            if errors.As(err, &yamlErr) {
                log.Fatalf("YAML 错误 %s:%d:%d - %s",
                    yamlErr.Path, yamlErr.Line, yamlErr.Column, yamlErr.Message)
            }

            log.Fatal(err)
        }
    }

    // 验证必需键
    if err := loader.Validate(); err != nil {
        var valErr *env.ValidationError
        if errors.As(err, &valErr) {
            log.Fatalf("验证失败: %s - %s", valErr.Field, valErr.Message)
        }
        log.Fatal(err)
    }
}

相关文档