Go 1.26: Build a Tiny JSON Service With the New new(expr)

Go 1.26 lets new() take a value, so optional JSON fields stop needing helper variables. Build a 35-line HTTP service in ten minutes and see the feature work, with every step explained from zero.

claude Routines

5 min read

By Ada Brackett — Solnyxus editorial voice. Drafted with AI assistance; reviewed by the team.

Every Go programmer has typed that tiny, slightly embarrassing two-step: declare a variable, then take its address, just to fill one optional field. Go 1.26 retires it. The Go 1.26 new expression feature means the built-in new now accepts a value, and I checked the official release notes on 14 October 2026 to be sure I had that right. We'll build a small web service with it. Ten minutes, one file, no dependencies.

What changed in Go 1.26?

Until now, new(T) took a type and handed back a pointer to a zero value of it. Want a pointer to 21.5? Two steps. Make a variable, then take its address.

In Go 1.26, new also accepts a value expression, and the new variable starts out holding that value. The release notes point at serialization as the sweet spot, because packages like encoding/json use a pointer to mean "this field is optional". A nil pointer means "unknown". A non-nil pointer means "here's a real value, even if it happens to be zero".

Go
t := 21.5
reading := Reading{Temp: &t} // the old way: a throwaway variable
reading = Reading{Temp: new(21.5)} // Go 1.26: one expression

The first version is what we all wrote for years. The second does the same job with no helper variable. (It needs Go 1.26 or newer, so if your team has stragglers on older toolchains, expect a grumpy compiler.)

Why optional fields use pointers

A plain float64 can't say "no value". Zero is a perfectly good temperature! So we reach for *float64: nil becomes JSON null, and anything else becomes a number. You'll see that pattern in nearly every API, and it's exactly where new(expr) saves you a line.

Side note: that "zero is a real answer" problem is the whole reason pointers got used this way in the first place. Back to the code.

Set up (two minutes)

You need Go 1.26 or newer. Check with go version. If yours is older, putting GOTOOLCHAIN=go1.26.0 in front of your commands tells Go to fetch the newer toolchain for you, and honestly I'd pick that over a global upgrade for an experiment like this, and I'll die on that hill.

Bash
mkdir tiny && cd tiny
go mod init tiny

That makes a folder and a go.mod file, which is where Go records your module name and Go version. Open go.mod and make sure it says go 1.26. The language feature only switches on at that version.

The whole service

Create main.go. I tested this exact file with Go 1.26.0 on Linux.

Go
package main

import (
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"time"
)

type Reading struct {
	Sensor string   `json:"sensor"`
	Temp   *float64 `json:"temp_c"`
	At     string   `json:"at"`
}

func readingHandler(w http.ResponseWriter, r *http.Request) {
	rd := Reading{
		Sensor: "kitchen",
		Temp:   new(21.5),
		At:     time.Now().UTC().Format(time.RFC3339),
	}
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(rd); err != nil {
		log.Println("encode:", err)
	}
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /reading", readingHandler)
	srv := &http.Server{Addr: "127.0.0.1:8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second}
	log.Println("listening on", srv.Addr)
	if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}

Let's take it in pieces.

The struct

Go
type Reading struct {
	Sensor string   `json:"sensor"`
	Temp   *float64 `json:"temp_c"`
	At     string   `json:"at"`
}

Three fields. The backtick tags tell encoding/json which names to print. Temp is a pointer so it can be null.

The one new line

Go
Temp:   new(21.5),

Here's the star, and yes, I got a little excited the first time I saw it in a real struct literal. new(21.5) allocates a float64 holding 21.5 and gives you its address. The constant 21.5 defaults to float64, so the type matches the field exactly. No fuss.

The handler

Go
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(rd); err != nil {
	log.Println("encode:", err)
}

We set the content type, then stream the struct as JSON straight into the response. If encoding fails (rare, but it happens), we log it rather than pretend it didn't.

Routing and the server

Go
mux.HandleFunc("GET /reading", readingHandler)

Since Go 1.22, patterns can carry the HTTP method. Only GET reaches this handler, and Go answers anything else with 405 Method Not Allowed for free.

Go
srv := &http.Server{Addr: "127.0.0.1:8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second}

We bind to the loopback address, so only your own machine can reach the service while you poke at it. The header timeout is a good habit too, because it stops slow clients from holding connections open forever.

Run it and check it worked

In one terminal:

Bash
go run .

In another:

Bash
curl -i localhost:8080/reading
curl -i -X POST localhost:8080/reading

The first should return 200 with a body like {"sensor":"kitchen","temp_c":21.5,"at":"2026-10-14T16:06:23Z"} (your timestamp will differ). The second should return 405. That's exactly what I saw when I ran it.

Common stumbles

If the compiler complains that new needs a type, your go.mod or toolchain is older than 1.26. Check go version and the go line in go.mod. If you get "address already in use", something else owns port 8080, so change the port in Addr.

Two other 1.26 items worth a look

The release notes also say the Green Tea garbage collector, an experiment in Go 1.25, is now on by default, and that a new errors.AsType function is a type-safe, generic version of errors.As. I haven't benchmarked the collector, so I won't make performance claims. Read the notes and measure your own workload.

Try this next

  1. Make Temp genuinely optional: return nil for a sensor named "garage" and confirm the JSON shows "temp_c":null.
  2. Add a POST /reading handler that decodes a Reading from the request body and echoes it back.
  3. Try errors.AsType in a small error-handling function and compare it with errors.As.

Go break the garage sensor on purpose, then come tell us what the null looked like.

Sources: Go 1.26 Release Notes (go.dev/doc/go1.26), checked 14 October 2026.