build: go build -ldflags "$(LDFLAGS)" -o bin/appconfig .
Reference implementation at ~/appconfig (Go 1.26, zero external deps). Two gaps closed:
Handlers *[]HandlerDef on the update structBefore: AppConfigUpdate had only Name; the admin PUT could not touch handlers, forcing callers to delete+recreate the whole app config.
After — model.go: every field is a pointer so "absent" (nil) is distinguishable from "explicitly set" (non-nil):
type AppConfigUpdate struct {
Name *string `json:"name,omitempty"`
Handlers *[]HandlerDef `json:"handlers,omitempty"` // pointer-to-slice: the fix
}
After — service.go: nil-preserving partial update; deep-copy exactly via append([]HandlerDef(nil), ...):
func (s *Service) Update(id string, req AppConfigUpdate) (AppConfig, error) {
s.mu.Lock()
defer s.mu.Unlock()
cfg, ok := s.configs[id]
if !ok {
return AppConfig{}, ErrNotFound
}
if req.Name != nil {
cfg.Name = *req.Name
}
if req.Handlers != nil { // nil pointer -> preserve; non-nil -> replace (even empty)
cfg.Handlers = copyHandlers(*req.Handlers) // append([]HandlerDef(nil), hs...) + nested Args copies
}
cfg.UpdatedAt = time.Now().UTC()
s.configs[id] = cfg
return cloneConfig(cfg), nil // returned value must not alias the store
}
func copyHandlers(hs []HandlerDef) []HandlerDef {
out := append([]HandlerDef(nil), hs...) // the prescribed pattern
for i := range out {
if out[i].Args != nil {
out[i].Args = append([]string(nil), out[i].Args...)
}
}
return out
}
Contract (verified empirically against encoding/json): JSON null and absent both decode to a nil pointer → preserve; {"handlers":[]} decodes to a non-nil pointer to an empty slice → clear; {"handlers":[...]} → replace. Wire demo:
PUT {"handlers":[{"name":"worker",...}]} -> replaces web handler in place
PUT {"name":"demo-v2"} -> worker preserved, name updated
PUT {"handlers":null} -> worker preserved (nil pointer)
PUT {"handlers":[]} -> handlers cleared
-version flag with ldflags injection// main.go
var version = "devel" // default; injected via -ldflags "-X main.version=..."
func main() {
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
if *showVersion {
fmt.Println(version)
os.Exit(0)
}
runServer()
}
# Makefile
VERSION ?= devel
LDFLAGS := -X main.version=$(VERSION)
build: go build -ldflags "$(LDFLAGS)" -o bin/appconfig .
version: go run -ldflags "$(LDFLAGS)" . -version
Verified by running the code in `~/appconfig`: - **`go test -race -count=1 ./...` — PASS, 9/9 tests**, covering: - **Way 1 (preserve):** `Handlers == nil` (name-only update) leaves handlers and their `Args` bit-identical; JSON `null` also preserves. - **Way 2 (replace):** non-nil pointer replaces the set — old handler gone; `[]` clears; non-nil pointer to a nil slice (programmatic caller) clears. - **Deep copy:** mutating the caller's slice/`Args` after `Update` and mutating the *returned* config both fail to leak into the store (store returns a clone; no shared backing arrays). - `ErrNotFound`, and HTTP-level `TestAdminPUTNilAndSetHandlers`/`TestAdminPUTNotFound` (httptest). - **`gofmt`/`go vet` clean.** - **Live server smoke test** on `ADDR=:18081` via curl confirmed all four wire cases (replace / preserve-on-omit / preserve-on-null / clear-on-empty) against the real `net/http` server. - **Version flag:** plain `go build` → `devel`; `go build -ldflags "-X main.version=v2.0.0-rc1"` → `v2.0.0-rc1`; `make version VERSION=v9.9.9` → `v9.9.9`; `make build` defaults to `devel`. Edge cases handled: `append(nil, empty...)` yields nil (documented in test — observable contract is `len == 0`, i.e. "cleared"); JSON `null` ≡ absent at the Go level (both nil pointer → preserve) since `encoding/json` nils the pointer for `null`; returned/GET configs are deep-cloned so no aliasing with the store or callers.
{"model": "gpt-5.6-sol/openai-codex", "problem_class": "go-backend-admin-api-update-slice-field", "result": "passed", "tests": 9}Reference implementation at ~/appconfig (Go 1.26, zero external deps). Two gaps closed:
Handlers *[]HandlerDef on the update structBefore: AppConfigUpdate had only Name; the admin PUT could not touch handlers, forcing callers to delete+recreate the whole app config.
After — model.go: every field is a pointer so "absent" (nil) is distinguishable from "explicitly set" (non-nil):
type AppConfigUpdate struct {
Name *string `json:"name,omitempty"`
Handlers *[]HandlerDef `json:"handlers,omitempty"` // pointer-to-slice: the fix
}
After — service.go: nil-preserving partial update; deep-copy exactly via append([]HandlerDef(nil), ...):
func (s *Service) Update(id string, req AppConfigUpdate) (AppConfig, error) {
s.mu.Lock()
defer s.mu.Unlock()
cfg, ok := s.configs[id]
if !ok {
return AppConfig{}, ErrNotFound
}
if req.Name != nil {
cfg.Name = *req.Name
}
if req.Handlers != nil { // nil pointer -> preserve; non-nil -> replace (even empty)
cfg.Handlers = copyHandlers(*req.Handlers) // append([]HandlerDef(nil), hs...) + nested Args copies
}
cfg.UpdatedAt = time.Now().UTC()
s.configs[id] = cfg
return cloneConfig(cfg), nil // returned value must not alias the store
}
func copyHandlers(hs []HandlerDef) []HandlerDef {
out := append([]HandlerDef(nil), hs...) // the prescribed pattern
for i := range out {
if out[i].Args != nil {
out[i].Args = append([]string(nil), out[i].Args...)
}
}
return out
}
Contract (verified empirically against encoding/json): JSON null and absent both decode to a nil pointer → preserve; {"handlers":[]} decodes to a non-nil pointer to an empty slice → clear; {"handlers":[...]} → replace. Wire demo:
PUT {"handlers":[{"name":"worker",...}]} -> replaces web handler in place
PUT {"name":"demo-v2"} -> worker preserved, name updated
PUT {"handlers":null} -> worker preserved (nil pointer)
PUT {"handlers":[]} -> handlers cleared
-version flag with ldflags injection// main.go
var version = "devel" // default; injected via -ldflags "-X main.version=..."
func main() {
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
if *showVersion {
fmt.Println(version)
os.Exit(0)
}
runServer()
}
# Makefile
VERSION ?= devel
LDFLAGS := -X main.version=$(VERSION)
build: go build -ldflags "$(LDFLAGS)" -o bin/appconfig .
version: go run -ldflags "$(LDFLAGS)" . -version
Verified by running the code in `~/appconfig`: - **`go test -race -count=1 ./...` — PASS, 9/9 tests**, covering: - **Way 1 (preserve):** `Handlers == nil` (name-only update) leaves handlers and their `Args` bit-identical; JSON `null` also preserves. - **Way 2 (replace):** non-nil pointer replaces the set — old handler gone; `[]` clears; non-nil pointer to a nil slice (programmatic caller) clears. - **Deep copy:** mutating the caller's slice/`Args` after `Update` and mutating the *returned* config both fail to leak into the store (store returns a clone; no shared backing arrays). - `ErrNotFound`, and HTTP-level `TestAdminPUTNilAndSetHandlers`/`TestAdminPUTNotFound` (httptest). - **`gofmt`/`go vet` clean.** - **Live server smoke test** on `ADDR=:18081` via curl confirmed all four wire cases (replace / preserve-on-omit / preserve-on-null / clear-on-empty) against the real `net/http` server. - **Version flag:** plain `go build` → `devel`; `go build -ldflags "-X main.version=v2.0.0-rc1"` → `v2.0.0-rc1`; `make version VERSION=v9.9.9` → `v9.9.9`; `make build` defaults to `devel`. Edge cases handled: `append(nil, empty...)` yields nil (documented in test — observable contract is `len == 0`, i.e. "cleared"); JSON `null` ≡ absent at the Go level (both nil pointer → preserve) since `encoding/json` nils the pointer for `null`; returned/GET configs are deep-cloned so no aliasing with the store or callers.
{"model": "gpt-5.6-sol/openai-codex", "problem_class": "go-backend-admin-api-update-slice-field", "result": "passed", "tests": 9}