How does Go 1.26's new expression improve optional-field modeling?
Question and context
Go 1.26 extends the built-in new so it can accept an expression and return a pointer to a new variable. Use an API struct to show how this simplifies optional-field initialization, then discuss JSON, generics, older compilers, and review boundaries.
What the interviewer evaluates
- Whether you distinguish
new(T), taking an address, and expression evaluation. - Whether you explain pointer optionals with zero values,
omitempty, and unmarshalling. - Whether you identify Go-version, generic-type, and readability risks.
- Whether you provide tests and a migration plan instead of only syntax.
Clarifying questions first
Data contract
Must the API distinguish a missing field, an explicit zero value, and explicit null? Do JSON clients rely on omitempty? Does the database also need three-state semantics?
Version and deployment
Have all build environments moved to Go 1.26? Do shared libraries, generators, or downstream modules still use an older toolchain?
Readability and policy
Does the team allow complex expressions as pointer values in business code? Should a simple helper remain for debugging and review?
A 30-second answer
Go 1.26 lets new(expr) place an expression result in a new variable and return its pointer, which is convenient for optional fields such as new(42). I would first decide whether the contract distinguishes missing, zero, and null, then choose pointers or a dedicated optional type. Before migration, pin the toolchain, add serialization tests, and keep expressions short for readability.
Deep-dive solution
1. Explain the semantic change
Traditional new(T) takes a type and returns a pointer to its zero value. Go 1.26 allows an expression operand: the compiler creates a variable, stores the expression result, and returns its pointer. The expression still follows normal Go evaluation rules, so side effects and order must remain visible.
2. Model optional fields
The following struct uses pointers to express whether a field was supplied:
type CreateUser struct {
Name string `json:"name"`
Age *int `json:"age,omitempty"`
Admin *bool `json:"admin,omitempty"`
}
req := CreateUser{
Name: "Ada",
Age: new(37),
Admin: new(false),
}Admin: new(false) differs semantically from Admin: nil. Whether false is emitted depends on serialization rules; a non-nil pointer commonly remains present despite its pointed-to zero value under omitempty, so test the API contract.
3. Compare alternatives
new(37) is shorter, while age := 37; &age is easier to inspect in a debugger. A ptrToInt(37) helper can centralize compatibility with older toolchains. Choose based on team policy, expression complexity, and call frequency; do not rewrite every pointer initialization just to use new syntax.
4. Handle generics and inference
The result type of new(expr) follows the expression. Complex generic expressions can make inference hard for readers. Public libraries should spell out types at boundaries; internal code should use simple constants or named variables and let the compiler and static checks catch type errors.
5. Respect JSON and database boundaries
A pointer distinguishes nil from non-nil but does not solve every distinction among database NULL, empty string, and zero. For PATCH-like APIs, test the contract explicitly: missing means no update, a non-nil zero means clear the value, and null is either accepted or rejected by the decoding layer.
6. Plan compatibility migration
Align go.mod, CI, container images, and code-generation steps on Go 1.26. If downstream users still compile with an older toolchain, retain the old form or isolate new code with a build matrix. Migrate a small package first, then run unit, serialization, and race tests before expanding.
7. Set review rules
Allow short, side-effect-free expressions in new; name complex computations first. Reviews should focus on three-state field semantics, post-escape lifetime, errors, and API compatibility rather than only checking that the syntax compiles.
Example of a strong answer
Go 1.26's new(expr) is convenient for turning a simple expression into an optional-field pointer, but it does not change pointer, JSON, or database three-state semantics. I would use *T to distinguish missing from an explicit zero, write PATCH and serialization contract tests, and migrate with one pinned toolchain. Complex expressions stay named, and an older-toolchain path remains available so readability and compatibility are deliberate choices.
Common mistakes
- Treating
new(0)as nil even though it returns a non-nil pointer. - Assuming
omitemptyautomatically removes a pointer to a zero value. - Committing Go 1.26 syntax before upgrading CI or downstream toolchains.
- Using pointers without defining missing, null, and zero semantics for PATCH.
- Putting complex side-effecting expressions inside
new. - Running only compilation checks and skipping JSON and database boundary tests.
Follow-up questions and answers
How do new(0) and new(int) differ?
new(0) returns a pointer to an int whose value is 0; new(int) returns a pointer to an int zero value. The values match, but the first demonstrates Go 1.26 expression operands.
Why not use value fields with defaults?
Value fields cannot distinguish missing from an explicit zero. For PATCH or compatibility with older clients, a pointer or dedicated optional type expresses the contract better.
Does this change escape analysis?
The compiler still decides whether the variable belongs on the stack or heap. Discuss observable semantics and benchmarks; do not infer heap allocation solely from the new keyword.
How do you support older Go versions?
Set the minimum version consistently in modules, CI, and release images. If simultaneous upgrade is impossible, retain v := value; &v or a helper and use a build matrix to keep new syntax out of old branches.
Should every field switch to new(expr)?
No. Use it when it makes optional initialization clearer. Complex expressions, public APIs, or code that benefits from debugger breakpoints can keep a named variable.