Skip to content

序列化 ​

使用 Marshal 和 Unmarshal 功能序列化/反序列化环境变量,支持 .env、JSON、YAML 格式转换。

基本序列化 ​

Map 序列化 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

func main() {
	data := map[string]string{
		"APP_NAME":    "my-app",
		"APP_VERSION": "1.0.0",
		"DEBUG":       "true",
	}

	// 序列化为 .env 格式
	result, err := env.Marshal(data, env.FormatEnv)
	if err != nil {
		panic(err)
	}

	fmt.Println(result)
	// 输出:
	// APP_NAME=my-app
	// APP_VERSION=1.0.0
	// DEBUG=true
}

JSON 格式 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

func main() {
	data := map[string]string{
		"HOST": "localhost",
		"PORT": "8080",
	}

	// 序列化为 JSON
	result, err := env.Marshal(data, env.FormatJSON)
	if err != nil {
		panic(err)
	}

	fmt.Println(result)
	// 输出:
	// {
	//   "HOST": "localhost",
	//   "PORT": 8080
	// }
}

YAML 格式 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

func main() {
	data := map[string]string{
		"DATABASE_HOST": "localhost",
		"DATABASE_PORT": "5432",
		"DATABASE_NAME": "myapp",
	}

	// 序列化为 YAML
	result, err := env.Marshal(data, env.FormatYAML)
	if err != nil {
		panic(err)
	}

	fmt.Println(result)
	// 输出:
	// DATABASE_HOST: localhost
	// DATABASE_NAME: myapp
	// DATABASE_PORT: "5432"
}

输出形态 ​

Marshal 的输出不是「扁平键原样输出」:两种结构化格式都会对键值做加工,了解这些行为才能正确解读导出结果。

JSON:下划线键重建嵌套对象 ​

Marshal 到 JSON 时,含下划线的键按 _ 拆分重建为嵌套对象(DATABASE_HOST → {"DATABASE": {"HOST": ...}});叶子值经类型推断还原——"8080" 回到 number、"true" 回到 bool、"null" 回到 null,浮点同样还原。

go
package main

import (
	"fmt"

	"github.com/cybergodev/env"
)

func main() {
	data := map[string]string{
		"DATABASE_HOST": "localhost",
		"DATABASE_PORT": "5432",
		"DEBUG":         "true",
	}

	result, err := env.Marshal(data, env.FormatJSON)
	if err != nil {
		panic(err)
	}

	fmt.Println(result)
	// 输出(键按字典序排序):
	// {
	//   "DATABASE": {
	//     "HOST": "localhost",
	//     "PORT": 5432
	//   },
	//   "DEBUG": true
	// }
}

这与加载方向对称:加载 JSON 时嵌套对象被扁平化为 DATABASE_HOST,Marshal 回 JSON 时再重建嵌套,往返后结构一致。

YAML:标量加引号保往返 ​

Marshal 到 YAML 时键保持扁平,但对读回时会被 YAML 强制转换类型的标量(数字、true/false、null/~ 等)自动加双引号,保证 Marshal → Unmarshal 往返后值仍为字符串:

go
package main

import (
	"fmt"

	"github.com/cybergodev/env"
)

func main() {
	data := map[string]string{
		"HOST":  "localhost",
		"PORT":  "8080",
		"DEBUG": "true",
	}

	result, err := env.Marshal(data, env.FormatYAML)
	if err != nil {
		panic(err)
	}

	fmt.Println(result)
	// 输出(键按字典序排序):
	// DEBUG: "true"
	// HOST: localhost
	// PORT: "8080"
}

这是 v1.2.3 起的行为:此前 PORT: 8080 不加引号,读回时会被 YAML 解析为数字,导致往返后类型漂移。

结构体序列化 ​

基本序列化 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

type Config struct {
	Host  string `env:"HOST"`
	Port  int64  `env:"PORT"`
	Debug bool   `env:"DEBUG"`
}

func main() {
	cfg := Config{
		Host:  "localhost",
		Port:  8080,
		Debug: true,
	}

	// 序列化结构体为 .env 格式
	result, err := env.Marshal(cfg, env.FormatEnv)
	if err != nil {
		panic(err)
	}

	fmt.Println(result)
	// 输出:
	// DEBUG=true
	// HOST=localhost
	// PORT=8080
}

嵌套结构体 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

type DatabaseConfig struct {
	Host string `env:"DB_HOST"`
	Port int64  `env:"DB_PORT"`
}

type AppConfig struct {
	Name     string `env:"APP_NAME"`
	Database DatabaseConfig
}

func main() {
	cfg := AppConfig{
		Name: "my-app",
		Database: DatabaseConfig{
			Host: "localhost",
			Port: 5432,
		},
	}

	result, err := env.Marshal(cfg, env.FormatEnv)
	if err != nil {
		panic(err)
	}

	fmt.Println(result)
}

MarshalStruct 函数 ​

将结构体转换为 map[string]string:

go
func MarshalStruct(v any) (map[string]string, error)

参数:

  • v - 结构体指针或值

返回:

  • map[string]string - 环境变量映射
  • error - 序列化错误
go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

type Config struct {
	Host  string `env:"HOST"`
	Port  int64  `env:"PORT"`
	Debug bool   `env:"DEBUG"`
}

func main() {
	cfg := Config{
		Host:  "localhost",
		Port:  8080,
		Debug: true,
	}

	// 转换为 map
	data, err := env.MarshalStruct(cfg)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", data)
	// 输出:map[DEBUG:true HOST:localhost PORT:8080]

	// 可用于导出到文件
	content, _ := env.Marshal(data, env.FormatEnv)
	fmt.Println(content)
}

反序列化 ​

Map 反序列化 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

func main() {
	// .env 格式字符串
	data := `
HOST=localhost
PORT=8080
DEBUG=true
`

	// 反序列化为 map
	result, err := env.UnmarshalMap(data, env.FormatEnv)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", result)
	// 输出:map[DEBUG:true HOST:localhost PORT:8080]
}

JSON 反序列化 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

func main() {
	jsonData := `{
        "API_KEY": "secret123",
        "API_URL": "https://api.example.com",
        "TIMEOUT": "30"
    }`

	result, err := env.UnmarshalMap(jsonData, env.FormatJSON)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", result)
	// 输出:map[API_KEY:secret123 API_URL:https://api.example.com TIMEOUT:30]
}

YAML 反序列化 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

func main() {
	yamlData := `
DATABASE_HOST: localhost
DATABASE_PORT: "5432"
DATABASE_USER: postgres
`

	result, err := env.UnmarshalMap(yamlData, env.FormatYAML)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", result)
	// 输出:map[DATABASE_HOST:localhost DATABASE_PORT:5432 DATABASE_USER:postgres]
}

结构体反序列化 ​

从 Map 反序列化 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

type Config struct {
	Host string `env:"HOST"`
	Port int64  `env:"PORT"`
}

func main() {
	data := map[string]string{
		"HOST": "example.com",
		"PORT": "443",
	}

	var cfg Config
	err := env.UnmarshalInto(data, &cfg)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", cfg)
	// 输出:{Host:example.com Port:443}
}

从字符串反序列化 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

type ServerConfig struct {
	Host    string `env:"SERVER_HOST"`
	Port    int64  `env:"SERVER_PORT"`
	Enabled bool   `env:"ENABLED"`
}

func main() {
	envData := `
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
ENABLED=true
`

	var cfg ServerConfig
	err := env.UnmarshalStruct(envData, &cfg, env.FormatEnv)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v\n", cfg)
	// 输出:{Host:0.0.0.0 Port:8080 Enabled:true}
}

自定义序列化 ​

两种自定义接口的生效范围

  • 字段级:结构体字段的自定义编解码,实现标准库 encoding.TextMarshaler / encoding.TextUnmarshaler(MarshalText() / UnmarshalText([]byte))。当结构体被 env.Marshal/env.UnmarshalInto 处理时,逐字段逻辑会识别这两个接口。
  • 顶层:env.Marshaler(MarshalEnv())与 env.Unmarshaler(UnmarshalEnv(map[string]string))接口仅在直接传给 env.Marshal/env.MarshalStruct/env.UnmarshalInto 的顶层值上生效;若传入的是含该类型字段的外层结构体,则不会被调用。

字段级:实现 encoding.TextMarshaler ​

go
package main

import (
	"fmt"
	"strings"

	"github.com/cybergodev/env"
)

type LogLevel string

// 实现 encoding.TextMarshaler —— 作为结构体字段序列化时会被调用
func (l LogLevel) MarshalText() ([]byte, error) {
	return []byte(strings.ToUpper(string(l))), nil
}

type LogConfig struct {
	Level LogLevel `env:"LOG_LEVEL"`
}

func main() {
	cfg := LogConfig{
		Level: LogLevel("debug"),
	}

	result, err := env.Marshal(cfg, env.FormatEnv)
	if err != nil {
		panic(err)
	}

	fmt.Println(result)
	// 输出:LOG_LEVEL=DEBUG
}

字段级:实现 encoding.TextUnmarshaler ​

go
package main

import (
	"fmt"

	"github.com/cybergodev/env"
)

type LogLevel string

// 实现 encoding.TextUnmarshaler —— 作为结构体字段反序列化时会被调用
func (l *LogLevel) UnmarshalText(text []byte) error {
	switch string(text) {
	case "debug", "info", "warn", "error":
		*l = LogLevel(text)
		return nil
	default:
		return fmt.Errorf("invalid log level: %s", string(text))
	}
}

type LogConfig struct {
	Level LogLevel `env:"LOG_LEVEL"`
}

func main() {
	data := map[string]string{
		"LOG_LEVEL": "info",
	}

	var cfg LogConfig
	err := env.UnmarshalInto(data, &cfg)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Level: %s\n", cfg.Level)
	// 输出:Level: info
}

顶层:实现 env.Marshaler / env.Unmarshaler ​

当直接把一个类型的值传给 env.Marshal / env.UnmarshalInto(而非作为外层结构体的字段)时,env.Marshaler / env.Unmarshaler 接口会在该顶层值上生效:

go
package main

import (
	"fmt"

	"github.com/cybergodev/env"
)

// 顶层类型直接实现 env.Marshaler
type EnvBlob string

func (e EnvBlob) MarshalEnv() ([]byte, error) {
	// 自定义整体序列化输出
	return []byte("APP_NAME=custom\nAPP_VERSION=2.0.0"), nil
}

func main() {
	// 直接序列化顶层值(不是外层结构体的字段)
	result, err := env.Marshal(EnvBlob(""), env.FormatEnv)
	if err != nil {
		panic(err)
	}

	fmt.Println(result)
	// 输出:
	// APP_NAME=custom
	// APP_VERSION=2.0.0
}

格式检测 ​

自动检测格式 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
)

func main() {
	// 自动检测格式
	format := env.DetectFormat("config.json")
	fmt.Println(format.String()) // json

	format = env.DetectFormat("settings.yaml")
	fmt.Println(format.String()) // yaml

	format = env.DetectFormat(".env")
	fmt.Println(format.String()) // dotenv

	// 使用 FormatAuto 自动检测
	data := `{"KEY": "value"}`
	result, _ := env.UnmarshalMap(data, env.FormatAuto)
	fmt.Println(result)
}

实用场景 ​

保存配置到文件 ​

go
package main

import (
	"github.com/cybergodev/env"
	"os"
)

func main() {
	cfg := map[string]string{
		"HOST": "localhost",
		"PORT": "8080",
	}

	// 序列化
	content, err := env.Marshal(cfg, env.FormatEnv)
	if err != nil {
		panic(err)
	}

	// 写入文件
	err = os.WriteFile(".env", []byte(content), 0644)
	if err != nil {
		panic(err)
	}
}

导出当前环境 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
	"os"
)

func main() {
	env.Load(".env")

	// 获取所有环境变量
	all := env.All()

	// 导出为 JSON
	content, err := env.Marshal(all, env.FormatJSON)
	if err != nil {
		panic(err)
	}

	fmt.Println(content)

	// 或写入文件
	os.WriteFile("env-export.json", []byte(content), 0644)
}

配置迁移 ​

go
package main

import (
	"fmt"
	"github.com/cybergodev/env"
	"os"
)

func main() {
	// 读取 JSON 配置
	jsonContent, _ := os.ReadFile("config.json")

	// 解析 JSON
	data, err := env.UnmarshalMap(string(jsonContent), env.FormatJSON)
	if err != nil {
		panic(err)
	}

	// 转换为 .env 格式
	envContent, err := env.Marshal(data, env.FormatEnv)
	if err != nil {
		panic(err)
	}

	// 保存为 .env 文件
	os.WriteFile(".env", []byte(envContent), 0644)

	fmt.Println("Config migrated from JSON to .env")
}

往返陷阱与格式自动检测 ​

$ 不转义:Marshal 输出再读回可能变形 ​

Marshal 不会转义值中的 $。若值包含 $VAR 或 ${VAR} 字面量,在默认开启 ExpandVariables 的情况下重新解析 Marshal 输出时,这些序列会被当作变量引用展开:

go
package main

import (
	"fmt"

	"github.com/cybergodev/env"
)

func main() {
	data := map[string]string{"PRICE": "100$USD"}

	out, err := env.Marshal(data, env.FormatEnv)
	if err != nil {
		panic(err)
	}
	fmt.Println(out)
	// 输出:PRICE=100$USD
	// 注意:若将 out 重新写回 .env 并以默认配置加载,$USD 会被尝试展开
}

规避方式:读取侧设置 cfg.ExpandVariables = false,或对含 $ 的值避免使用 Marshal 往返。

MarshalStruct 的字段取舍 ​

结构体序列化时,编组为空字符串的字段会被省略(空字符串、nil 指针、空切片);标量零值(0、false)保留——因为它们不会编组为空字符串。读取侧用 envDefault 标签补默认值即可对齐。

键排序保证 ​

Marshal 的输出键始终按字典序排序,与输入 map/结构体的字段顺序无关——这保证同一份配置的输出是稳定的,便于 diff 与缓存。

FormatAuto 内容检测规则 ​

UnmarshalMap/UnmarshalStruct 传入 FormatAuto 时按内容(而非扩展名)检测格式:

特征判定
空白输入.env
首字符为 { 或 [JSON
首个有效行以 - 开头,或包含 : (冒号 + 空格)YAML
包含 =.env

注意 : 优先于 = 判定:形如 connection: host=db port=5432 的行会被判为 YAML(而非 .env)。

IsMarshalError 辅助判断 ​

go
if _, err := env.Marshal(data); err != nil {
	if env.IsMarshalError(err) {
		// 序列化错误:输入类型不支持或字段转换失败
	}
}

相关文档 ​