goenv: Drei Funktionen, Eine Env-Variable, Null Scheiß Drumherum

Jeder Service, den ich schreibe, braucht genau eine Information, bevor er irgendetwas anderes tut: läuft das Ding in Prod, oder fummelt irgendein armer Irrer (ich, um 2 Uhr nachts, eine halbe Flasche intus) immer noch lokal daran herum. Das ist die komplette Anforderung. Jahrelang lebte die Antwort als os.Getenv("ENV") und ein zweizeiliges if, in jede main.go kopiert, die ich je geschrieben habe, jedes Mal ein bisschen anders geschrieben. Also habe ich getan, was jeder vernünftige Entwickler bei einem Zweizeilenproblem tut: ich habe ein ganzes Paket dafür geschrieben, ihm ein README mit FAQ-Abschnitt verpasst, elf Releases getaggt und es mit voller Testabdeckung ausgeliefert. Das ist goenv.

Das Problem, Das Eigentlich Keines Ist

Es gibt keines, und das ist der ganze Witz. os.Getenv("ENV") sind sechzehn Zeichen. Es mit "dev" zu vergleichen, sind elf weitere. Du könntest die ganze Prüfung inline schreiben, in weniger Zeit, als du zum Lesen dieses Satzes gebraucht hast. Nur hättest du sie dann inline, in jedem Service, jedes Mal ein bisschen anders geschrieben, os.Getenv("ENVIRONMENT") hier, APP_ENV dort, jemand, der in einem dritten Repo auf == "development" statt auf "dev" prüft, und schon ist deine “bin ich in Prod?”-Logik ein Minenfeld, verstreut über jeden Service, den du besitzt, jeder auf seine ganz eigene Weise falsch verdrahtet. goenv existiert, damit diese Frage genau eine Implementierung hat, an genau einer Stelle definiert, überall importiert, wo es zählt.

Die Komplette Implementierung

Hier ist die komplette Implementierung. Kein Auszug, die ganze Datei:

package goenv
import (
	"os"
)
const EnvVarName = "ENV"
type Type = string
const (
	Prod Type = "prod"
	Dev  Type = "dev"
)
func Get() Type {
	e := os.Getenv(EnvVarName)
	switch e {
	case Dev:
		return Dev
	default:
		return Prod
	}
}
func IsProd() bool {
	return Get() == Prod
}
func IsDev() bool {
	return Get() == Dev
}

Ein Import. Nicht mal fmt. Ein Switch mit einem einzigen echten Fall und einem Default, der immer auf Prod landet. Setz ENV=dev und du bekommst Dev zurück. Setz es auf irgendetwas anderes, prod, staging, test, einen leeren String, einen Tippfehler oder gar nichts, und du bekommst Prod. goenv traut dir nicht zu, dass du es richtig geschrieben hast, also versucht es gar nicht erst zu raten. Wenn es nicht ausdrücklich dev ist, ist es Prod. Kein Mittelweg, kein Vertrauensvorschuss.

Die exportierte Oberfläche sind genau drei Funktionen: Get() liefert den aktuellen Type (“prod” oder “dev”), und IsProd() / IsDev() sind die beiden booleschen Bequemlichkeiten, die nur Get() aufrufen und vergleichen. Drei Konstanten stützen sie, EnvVarName (“ENV”), Prod und Dev, dazu ein Type-Alias. Und bei dem will ich ehrlich sein, denn er steht da als type Type = string, mit Gleichheitszeichen, was ihn zu einem echten Alias macht, nicht zu einem definierten Typ. Er ist in beide Richtungen mit string austauschbar. Du holst da null Sicherheit zur Compilezeit raus, nichts hindert dich daran, "banana" zu übergeben, wo ein Type erwartet wird. Das ist Dokumentation mit Zwischenschritten, und etwas anderes zu behaupten wäre genau die Sorte Sache, über die sich dieses Paket lustig machen soll. Das ist die ganze API. Kein Config-Struct, keine funktionalen Optionen, kein Interface zum Implementieren, kein WithLogger(). Drei Funktionen, und du kennst sie längst alle.

Benutzung (Viel Gibt Es Nicht zu Benutzen)

go get github.com/psyb0t/goenv
package main
import (
	"fmt"
	"github.com/psyb0t/goenv"
)
func main() {
	if goenv.IsProd() {
		fmt.Println("don't fuck this up")
	}
	if goenv.IsDev() {
		fmt.Println("break whatever you want")
	}
}

Und auf der Deployment-Seite ist es einfach eine Umgebungsvariable wie jede andere:

export ENV=dev   # you're developing, go break shit
export ENV=prod  # you're in production, don't
export ENV=      # also production, because paranoia is a feature

Das ist genau der Trick, den servicepack für seine eigene Umgebungserkennung benutzt, dasselbe Paket, dieselbe ENV-Variable, derselbe Default nach dem Motto “solange du nicht ausdrücklich dev sagst, bist du in Prod”. Eine Abhängigkeit, ein Verhalten, überall wiederverwendet statt pro Service neu erfunden.

Was Wirklich in der Kiste Ist

  • Null Abhängigkeiten, go.mod besteht aus einer Modulzeile und einer Go-Version, sonst nichts. Der einzige Import in ganz goenv.go ist os.
  • Echte Testabdeckung, keine README-Angeberei, ich habe mir den Quelltext gezogen und go test -cover ./... selbst laufen lassen: 100.0% of statements. Zehn Untertests über drei Testfunktionen decken jeden Zweig ab, dev, prod, leerer String, und ein nicht erkannter Wert (“staging”), der ebenfalls auf Prod durchfallen muss, weil die Tests prüfen, dass die Regel “alles, was nicht ausdrücklich dev ist, ist Prod” tatsächlich hält.
  • Fällt auf Prod zurück, nachweislich, es steht direkt im Switch: der einzige Pfad, der Dev zurückgibt, ist eine exakte Übereinstimmung mit ENV=dev. Ungesetzt, leer, falsch geschrieben, “staging”, “test”, egal was, alles löst sich zu Prod auf. Das Paket traut dir nicht und macht kein Geheimnis daraus.
  • Elf getaggte Releases, v1.0.0 (“fuck yeah prod or dev”), v1.0.1 (“peen”), v1.0.2, das einen ClawHub-Agent-Skill dazubrachte, und acht weitere, die keine einzige Zeile Bibliothekscode geändert haben. Ja. Ein Paket mit drei Funktionen, das eine Umgebungsvariable liest, liefert jetzt einen dokumentierten Agent-Skill, damit man einer KI erklären kann, wie sie IsProd() aufruft. Die Skill-Datei ist länger als die Bibliothek, die sie dokumentiert. Dann kamen Badges, Lizenzhinweise, ein Codeberg-Mirror, eine Aufteilung der CI-Pipeline, Go 1.26, und ein Release, dessen gesamtes Changelog darin besteht, dass die Oberflächenzusammenfassung im README den Type zu erwähnen vergessen hatte. goenv.go hat genau einen Commit in seiner ganzen Geschichte, den ersten, jedes Release nach dem ersten ist also Verpackung, Papierkram oder eine Tippfehlerkorrektur, was zugleich der lustigste und der korrekteste verfügbare Ausgang ist. Alle elf echt, alle elf durch dieselbe CI-Pipeline geschickt wie jedes andere Paket, das ich ausliefere: go vet, go test -race, und eine Coverage-Schranke, die im Makefile auf mindestens 90% steht, und seit v1.0.2 zusätzlich ein tag-gesteuerter Publish-Job, der den Skill nach bestandenem Lint und Test auf ClawHub schiebt, eine Latte, die dieses Paket um zehn volle Punkte überspringt, ohne dabei irgendwas zu tun.

Unterm Strich

Wenn du mehr als ein Bit Information aus deiner Umgebung brauchst, Typen, Defaults, Structs, das ganze Programm, dann lies lieber über gonfiguration, das ist die erwachsene Version dieser Idee. goenv sitzt am anderen Ende des Spektrums: es beantwortet eine Frage, beantwortet sie jedes Mal gleich, und tut nicht so, als wäre es mehr als das.

Hol es dir von GitHub, oder schreib os.Getenv("ENV") == "dev" selbst wie ein normaler Mensch. Geht beides. Ich bin nicht dein Chef.

Rein Damit In Deinen Agenten

Ja, das Drei-Funktionen-Paket hat ein Plugin. Nein, ich werde nicht weiter darüber reden. Alles unter .agents/ ist in einem einzigen Marketplace katalogisiert, also sind es zwei Befehle:

claude plugin marketplace add psyb0t/agents
claude plugin install goenv@psyb0t

Codex nutzt denselben Marketplace mit einem anderen Verb, codex plugin add goenv@psyb0t, weil es kein codex plugin install gibt. Er findet den Skill außerdem von allein in einem Checkout des Repos, da er .agents/skills/ nativ scannt, ganz ohne Installation.