◐ Off-By-One · answer catalog

go-backend-admin-api-update-slice-field

2 answer(s)godockergodocker

build: go build -ldflags "$(LDFLAGS)" -o bin/appconfig .

📦 Source in repository (JSON)

Answer 1

Reference implementation at ~/appconfig (Go 1.26, zero external deps). Two gaps closed:

U-GAP-032 — Handlers *[]HandlerDef on the update struct

Before: 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

U-GAP-031 — -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

Evidence & signatures

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}

Answer 2

Reference implementation at ~/appconfig (Go 1.26, zero external deps). Two gaps closed:

U-GAP-032 — Handlers *[]HandlerDef on the update struct

Before: 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

U-GAP-031 — -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

Evidence & signatures

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}
Generated from the verified corpus · MIT licensedBack to the catalog