功能说明
本地开发和容器编排常用 .env 文件管理配置,但格式比 JSON 宽松,手写解析容易漏掉引号、转义和注释。这个解析器逐行处理:忽略空行与整行注释,去掉可选的 export 前缀,按第一个等号切分键值,再按值的写法解码。
单引号内的内容原样保留,适合包含空格和特殊符号的口令;双引号使用 Go 的转义规则,支持换行和引号;未加引号的值会去掉行尾以空格加井号开头的注释。键名必须符合常见的环境变量命名规范,非法键直接报错并带行号。
关键代码
parseEnvValue 先检查首尾是否成对的单引号或双引号;双引号分支调用 strconv.Unquote,能正确处理反斜杠转义,引号不配对时返回错误。无引号分支只把空格加井号视为注释起点,因此口令里的井号不会被误删。ParseEnv 用 strings.Cut 在第一个等号处切分,值里可以继续包含等号,例如连接串参数。返回的映射覆盖重复键的旧值。
使用注意
该解析器不做变量插值,形如美元符号引用其他变量的写法会原样保留。不支持跨行值,长证书建议挂载文件而不是塞进环境变量。解析成功不代表配置可用,数值和地址仍需业务校验。环境变量会出现在进程信息中,敏感值应使用密钥管理服务。读取真实文件时要限制权限并避免把内容写进日志。
验证方式
示例文档解析后包含五个键:接口地址、应用名、空值、带空格的口令和端口。应用名的双引号内包含转义引号,输出应保留为 Demo 加引号加 API;口令原样输出,端口值只保留 8080,行尾注释被去除。非法的 1BAD 键会返回错误,提示打印为 true。
代码示例
package main
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
)
var envKeyPattern = regexp.MustCompile("^[A-Za-z_][A-Za-z0-9_]*$")
func parseEnvValue(raw string) (string, error) {
if len(raw) >= 2 && raw[0] == '\'' && raw[len(raw)-1] == '\'' {
return raw[1 : len(raw)-1], nil
}
if len(raw) >= 2 && raw[0] == '"' && raw[len(raw)-1] == '"' {
unquoted, err := strconv.Unquote(raw)
if err != nil {
return "", fmt.Errorf("bad quoted value: %w", err)
}
return unquoted, nil
}
if index := strings.Index(raw, " #"); index >= 0 {
raw = strings.TrimSpace(raw[:index])
}
return raw, nil
}
func ParseEnv(data []byte) (map[string]string, error) {
values := make(map[string]string)
for number, raw := range strings.Split(string(data), "\n") {
line := strings.TrimSpace(strings.TrimSuffix(raw, "\r"))
if line == "" || strings.HasPrefix(line, "#") {
continue
}
line = strings.TrimSpace(strings.TrimPrefix(line, "export "))
key, value, ok := strings.Cut(line, "=")
if !ok {
return nil, fmt.Errorf("line %d: missing '='", number+1)
}
key = strings.TrimSpace(key)
if !envKeyPattern.MatchString(key) {
return nil, fmt.Errorf("line %d: invalid key %q", number+1, key)
}
parsed, err := parseEnvValue(strings.TrimSpace(value))
if err != nil {
return nil, fmt.Errorf("line %d: %w", number+1, err)
}
values[key] = parsed
}
return values, nil
}
func main() {
document := []byte("# service settings\n" +
"export API_URL=https://api.example.test\n" +
"export APP_NAME=\"Demo \\\"API\\\"\"\n" +
"EMPTY=\n" +
"DB_PASSWORD='p@ss word'\n" +
"PORT=8080 # public port\n")
values, err := ParseEnv(document)
if err != nil {
panic(err)
}
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf("%s=%q\n", key, values[key])
}
_, err = ParseEnv([]byte("1BAD=x"))
fmt.Println("invalid key rejected:", err != nil)
}