Skip to content
docuconf
docuconf on GitHub

Get started / Go

docuconf for Go

Your config struct stays a caarlos0/env struct. docuconf adds struct tags for secrets and constraints, takes descriptions from doc comments, and ships the docuconf CLI that exports and vets contracts.

Builds on
caarlos0/env v11
Package
github.com/docuconf/docuconf-go
Requires
Go 1.24
Status
v0.1 alpha, not yet released · conformance 134 of 134
Source
docuconf-go · the orders example

Not published yet

Every docuconf SDK is a v0.1 alpha and none is on a package registry yet, so step 1 installs from the main branch. After the first release it will be go get github.com/docuconf/docuconf-go@latest.

The code below is the orders service, the example every SDK ships: six variables, one of them a secret, checked the same way in every language. Compare it across languages.

1.Install

Add the module and install the CLI. Until the first release, both come from the main branch.

go get github.com/docuconf/docuconf-go@main
go install github.com/docuconf/docuconf-go/cmd/docuconf@main

2.Declare your configuration

Keep the struct you would write for caarlos0/env. The env and envDefault tags are caarlos0's own; docuconf adds min, max, values, schemes and minItems, a docuconf.Secret type, and file inputs such as docuconf.TLSKeyPair. The first paragraph of each doc comment is the description, and the rest its details. The struct lives in its own package because the exporter imports it.

internal/config/config.go
// Package config is the orders service's configuration: an ordinary
// caarlos0/env struct with docuconf's tags. It lives in its own package
// because `docuconf export` imports it, and package main cannot be imported.
package config

import (
	"time"

	"github.com/docuconf/docuconf-go"
)

// Config is everything the orders service reads at boot.
// Doc comments become the descriptions in the contract: the first
// paragraph is the description, and any later paragraphs are its details.
type Config struct {
	// HTTP listen port.
	Port int `env:"PORT" envDefault:"8080" min:"1" max:"65535"`

	// Minimum log level emitted.
	LogLevel string `env:"LOG_LEVEL" envDefault:"info" values:"debug,info,warn,error"`

	// Postgres connection string for the orders database.
	DatabaseURL docuconf.Secret `env:"DATABASE_URL,required" schemes:"postgres" maxLength:"2048"`

	// Origins allowed to call the API from a browser.
	AllowedOrigins []string `env:"ALLOWED_ORIGINS" envDefault:"http://localhost:3000" minItems:"1"`

	// Time limit for handling one request.
	RequestTimeout time.Duration `env:"REQUEST_TIMEOUT" envDefault:"30s" min:"1s" max:"5m"`

	// Number of background workers processing orders.
	//
	// Each worker holds one database connection, so keep it below the
	// database's connection limit divided by the number of replicas.
	// Raise it when the order queue grows faster than it drains.
	WorkerCount int `env:"WORKER_COUNT" envDefault:"4" min:"1" max:"64"`

	// Certificate to serve HTTPS with. Without it, the service serves HTTP.
	TLS docuconf.TLSKeyPair `file:"serving-tls" path:"/etc/orders/tls" dnsNames:"orders.example.com" minRemaining:"720h" reload:"watch"`

	// Discount codes accepted at checkout.
	Discounts docuconf.ConfigFile[Discounts] `file:"discounts" path:"/etc/orders/discounts/discounts.yaml"`
}

// Discounts is the content of the discounts file.
type Discounts struct {
	// Percent off for each discount code.
	Codes map[string]int `json:"codes" yaml:"codes"`
}

3.Load it at boot

docuconf.ParseOrExit is caarlos0's env.ParseAs plus every docuconf check and file input, run once at startup. On a bad configuration it prints every problem and exits 1; docuconf.Parse returns the error instead.

main.go
cfg := docuconf.ParseOrExit[config.Config]()

4.Run it, and see an error

Run the example with a valid environment:

DATABASE_URL=postgres://orders:pw@localhost:5432/orders go run .

With PORT=0 and no DATABASE_URL, the service refuses to start. It lists every problem with its error code, exits 1, and writes the same lines to /dev/termination-log so kubectl describe pod shows them.

Terminal
$ PORT=0 go run .
docuconf: 2 configuration problems:
  PORT: 0 is below min 1 (out_of_range)
  DATABASE_URL: is required but not set (missing_required)
exit status 1

5.Test your configuration

ParseWithOptions takes an explicit environment, so a test never reads or changes the process environment.

internal/config/config_test.go
package config_test

import (
	"errors"
	"testing"

	"github.com/docuconf/docuconf-go"
	"github.com/docuconf/docuconf-go/examples/orders/internal/config"
)

// Load from an explicit environment: the process environment is not read or changed.
func load(env map[string]string) (config.Config, error) {
	return docuconf.ParseWithOptions[config.Config](docuconf.Options{
		Environment:    env,
		TerminationLog: "-", // don't write /dev/termination-log from tests
	})
}

func TestDefaults(t *testing.T) {
	cfg, err := load(map[string]string{"DATABASE_URL": "postgres://orders@db/orders"})
	if err != nil {
		t.Fatal(err)
	}
	if cfg.Port != 8080 || cfg.WorkerCount != 4 {
		t.Errorf("got port %d, %d workers", cfg.Port, cfg.WorkerCount)
	}
}

func TestRejectsBadValues(t *testing.T) {
	_, err := load(map[string]string{"PORT": "70000"})
	var verr *docuconf.ValidationError
	if !errors.As(err, &verr) {
		t.Fatalf("want a ValidationError, got %v", err)
	}
	if !verr.Has(docuconf.CodeOutOfRange) || !verr.Has(docuconf.CodeMissingRequired) {
		t.Errorf("got %v", verr.Violations)
	}
}
go test ./internal/config/

6.Export the contract

The CLI reads the struct by static analysis, so export needs no running program and no environment. Commit contract.cue, or publish it with the image; then the platform checks its values against it with docuconf vet.

docuconf export -pkg ./internal/config -type Config -name orders-api -package orders -o contract.cue
contract.cue
// Code generated by docuconf. DO NOT EDIT.
package orders

import "docuconf.dev/contract"

contract.#Contract & {
	apiVersion: "docuconf.dev/v1alpha1"
	kind:       "ConfigContract"
	metadata: {
		name: "orders-api"
		generator: {
			language: "go"
			sdk:      "docuconf-go"
			version:  "0.1.0"
		}
	}
	vars: {
		ALLOWED_ORIGINS: {
			type:        "list"
			description: "Origins allowed to call the API from a browser"
			default: ["http://localhost:3000"]
			items:     "string"
			encoding:  "csv"
			separator: ","
			minItems:  1
		}
		DATABASE_URL: {
			type:        "url"
			description: "Postgres connection string for the orders database"
			required:    true
			secret:      true
			schemes: ["postgres"]
			maxLength: 2048
		}
		LOG_LEVEL: {
			type:        "enum"
			description: "Minimum log level emitted"
			default:     "info"
			values: ["debug", "info", "warn", "error"]
		}
		PORT: {
			type:        "int"
			description: "HTTP listen port"
			default:     8080
			min:         1
			max:         65535
		}
		REQUEST_TIMEOUT: {
			type:        "duration"
			description: "Time limit for handling one request"
			default:     "30s"
			min:         "1s"
			max:         "5m"
			encoding:    "go"
		}
		WORKER_COUNT: {
			type:        "int"
			description: "Number of background workers processing orders"
			details:     "Each worker holds one database connection, so keep it below the database's connection limit divided by the number of replicas. Raise it when the order queue grows faster than it drains."
			default:     4
			min:         1
			max:         64
		}
	}
	files: {
		discounts: {
			type:        "config"
			format:      "yaml"
			description: "Discount codes accepted at checkout"
			path:        "/etc/orders/discounts/discounts.yaml"
			schema: {
				type: "object"
				required: ["codes"]
				additionalProperties: false
				properties: {
					codes: {
						type:        "object"
						description: "Percent off for each discount code"
						additionalProperties: {
							type: "integer"
						}
					}
				}
			}
		}
		"serving-tls": {
			type:        "tls"
			description: "Certificate to serve HTTPS with. Without it, the service serves HTTP"
			secret:      true
			path:        "/etc/orders/tls"
			reload:      "watch"
			dnsNames: ["orders.example.com"]
			minRemaining: "720h"
		}
	}
}

The platform then checks each environment's values against contract.cue before it deploys, with docuconf vet, the Helm chart or CUE. See How it works.

7.net/http, and any framework

Go has no framework config layer to plug into: Parse returns a typed struct, and you pass it to whatever you start, whether net/http, chi, Echo or gin. The CLI also validates values before deploy: docuconf vet checks a values file against the contract, as the hero on the homepage shows, and docuconf render turns valid values into the pod's env.

Terminal
$ docuconf vet -contract contract.cue -values values.yaml
DATABASE_URL: is secret, so it must come from a secretKeyRef, written {secretKeyRef: {name: <secret>, key: <key>}}, or an injector, never a literal or another reference
DATABSE_URL: is not declared in the contract (check the spelling)
PORT: 70000 is above max 65535

Next

Every snippet on this page is compiled or run against docuconf-go's main branch in CI. Facts checked 2026-10-08.