Skip to content

变量展开 ​

env 库支持在配置文件中使用变量引用,实现配置复用和动态值替换。

启用变量展开 ​

go
cfg := env.DefaultConfig()
cfg.ExpandVariables = true  // 默认启用

loader, _ := env.New(cfg)
loader.LoadFiles(".env")

基本语法 ​

简单引用 ​

bash
# 引用其他变量
BASE_URL=https://api.example.com
API_URL=${BASE_URL}/v1
# API_URL 展开为: https://api.example.com/v1

# 简写语法
HOST=localhost
URL=$HOST:8080
# URL 展开为: localhost:8080

默认值语法 ​

语法说明
${VAR:-default}如果 VAR 不存在,使用 default
${VAR:=default}如果 VAR 不存在,使用 default(同 :-)
${VAR:?error}如果 VAR 不存在或为空,返回错误

自引用限制

:-、:=、:? 引用的变量必须与被赋值的键不同。形如 KEY=${KEY:-default} 的自引用会被识别为循环引用,加载时报 ErrExpansionDepth 错误。为某键设置默认值请直接赋值字面量(KEY=default),或引用其他变量(见下方示例)。


语法详解 ​

${VAR:-default} - 使用默认值 ​

最常见的默认值语法。当变量不存在时使用默认值,变量存在(即使值为空)则使用原值:

bash
# HOST 已定义,使用其值
HOST=localhost
PRIMARY_HOST=${HOST:-127.0.0.1}
# PRIMARY_HOST 展开为: localhost

# TIMEOUT 未定义时使用默认值 "30s"
TIMEOUT_VALUE=${TIMEOUT:-30s}
# TIMEOUT_VALUE 展开为: 30s

# 嵌套默认值
DB_HOST=localhost
DB_URL=${DB_HOST}:${DB_PORT:-5432}
# DB_HOST=localhost 且 DB_PORT 未定义时
# DB_URL 展开为: localhost:5432

使用场景:

  • 可选配置项的默认值
  • 开发/生产环境统一配置

${VAR:=default} - 使用默认值 ​

行为与 ${VAR:-default} 相同,当变量不存在时使用默认值:

bash
# DEBUG 未定义时使用 "false"
DEBUG_VALUE=${DEBUG:=false}

# CACHE_TTL 未定义时使用默认值
CACHE_TTL_VALUE=${CACHE_TTL:=3600}

与 :- 的关系

${VAR:=default} 在本库中与 ${VAR:-default} 行为完全相同。当变量不存在时,使用默认值作为展开结果。:= 不会将默认值写回变量存储。


${VAR:?error} - 错误提示 ​

如果变量不存在或为空则返回错误:

bash
# 如果 DATABASE_URL 未定义,加载失败并显示错误
DB_URL=${DATABASE_URL:?Database URL is required}

# 如果 API_TOKEN 未定义,报错
AUTH_TOKEN=${API_TOKEN:?API_TOKEN must be set}

使用场景:

  • 必需配置项验证
  • 早期失败,避免运行时错误

转义 ​

转义美元符号 ​

使用 $$ 表示字面量 $:

bash
# 价格配置
PRICE=$$99.99
# 展开为: $99.99

# 包含 $ 的字符串
MESSAGE=Price is $$100
# 展开为: Price is $100

引号与展开 ​

变量展开发生在引号剥离之后的统一后处理阶段,单引号与双引号都不影响变量展开。例如 SINGLE='${BASE}'(BASE=hello)展开后的值为 hello,与双引号行为一致;若被引用的变量未定义(如 LITERAL='${NO_EXPANSION}'),结果为空字符串,而非保留 ${NO_EXPANSION} 字面量。

单引号与双引号的区别仅在字面解析:双引号处理 \n、\t 等转义序列,单引号原样保留(不转义)。

注意

不要用引号来"禁止展开"。如需保留 ${VAR} 字面量,请使用以下方式:

bash
# 方式一:转义美元符号($$ 展开为字面 $)
LITERAL='$${NO_EXPANSION}'
# 值为: ${NO_EXPANSION}
go
// 方式二:关闭全局变量展开
cfg := env.DefaultConfig()
cfg.ExpandVariables = false

嵌套展开 ​

变量可以嵌套引用:

bash
# 基础配置(避免使用内置禁止键 ENV,改用 DEPLOY_ENV)
APP_NAME=myapp
DEPLOY_ENV=production

# 嵌套引用
DB_HOST=db.${DEPLOY_ENV}.example.com
# 展开为: db.production.example.com

API_URL=https://${APP_NAME}.${DEPLOY_ENV}.api.example.com
# 展开为: https://myapp.production.api.example.com

循环检测 ​

库自动检测循环引用并返回错误:

bash
# 循环引用(错误)
A=${B}
B=${A}

# 加载时会返回 ErrExpansionDepth 错误

展开深度限制 ​

默认最大展开深度为 5,硬性上限为 20:

go
cfg := env.DefaultConfig()
cfg.MaxExpansionDepth = 10  // 自定义深度
常量值说明
DefaultMaxExpansionDepth5默认值(公开 API)

提示

硬性上限为 20(内部限制)。配置的 MaxExpansionDepth 不能超过此限制。


完整示例 ​

bash
# .env 文件

# 基础配置(避免使用内置禁止键 ENV)
APP_NAME=myapp
DEPLOY_ENV=development
DEBUG=true

# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_NAME=${APP_NAME}
DB_URL=postgres://${DB_HOST}:${DB_PORT}/${DB_NAME}

# API 配置
API_BASE=https://api.${DEPLOY_ENV}.example.com
API_URL=${API_BASE}/v1

# 日志配置
LOG_LEVEL=info

# 价格(转义)
PRICE=$$99.99
go
package main

import (
	"fmt"
	"log"

	"github.com/cybergodev/env"
)

func main() {
	cfg := env.DefaultConfig()
	cfg.ExpandVariables = true

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

	err = loader.LoadFiles(".env")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("DB_URL:", loader.GetString("DB_URL"))
	fmt.Println("API_URL:", loader.GetString("API_URL"))
	fmt.Println("PRICE:", loader.GetString("PRICE"))
}

展开作用域(安全隔离) ​

默认情况下,变量展开按「先文件、后进程环境」的顺序解析引用——${VAR} 在文件内找不到时,会回退到进程环境(os.LookupEnv)继续查找。这对传统 dotenv 语义是正确的,但当配置文件来自不完全可信的来源(用户上传、外部系统下发)时会带来风险:文件中写入 ${AWS_SECRET_ACCESS_KEY},加载后即可把进程中无关的机密「捕获」进变量值,随后可能被日志、序列化或导出操作带出。

ParsingConfig.ExpansionScope 用于控制这一行为:

取值行为适用场景
ExpansionFileThenProcess(默认)先查文件内变量,缺失时回退进程环境可信配置文件,传统 dotenv 语义
ExpansionFileOnly仅文件内变量可见;进程环境引用展开为空字符串不可信配置来源,防止机密探测(SEC-03)
go
cfg := env.DefaultConfig()
// 仅允许引用文件内变量,阻断对进程环境的读取
cfg.ExpansionScope = env.ExpansionFileOnly
loader, _ := env.New(cfg)

SEC-03

ExpansionFileOnly 是防止配置文件「窃取」进程机密的安全开关。若你的应用会加载外部提供的配置文件,建议始终启用该作用域。

注意:ExpansionScope 是枚举而非布尔字段,即使 Config 只设置了这一个字段,IsZero() 也会正确识别其为已初始化的配置,不会被默认值静默覆盖。

相关文档 ​