Interfaces, iota, and Why json.Unmarshal Needs a Pointer
A method is just a function with a receiver, a nil interface panics differently than a nil pointer, and iota isn't a TypeScript enum — three Go idioms I had to unlearn my TS assumptions for.
TypeScript interfaces describe shape. A class bundles its methods with its fields by syntax. enum is a real type. None of those assumptions carried over cleanly when I started writing Go.
A Method Is a Function With a Receiver, Nothing Enforces Locality
type Point struct { X, Y int}
func (p Point) String() string { return fmt.Sprintf("(%d, %d)", p.X, p.Y)}(p Point) is the receiver, and under the hood this is just a function that takes a Point as its first argument — the dot syntax (p.String()) is sugar over that. Nothing in the language requires this method to sit next to type Point struct, or even live in the same file. A TypeScript class Point { toString() {...} } bundles fields and methods because the syntax forces it. Go’s version is a convention the team enforces, not something the compiler checks, so I’ve started treating “keep the type and its methods in one file” as a rule I owe the next reader, not something Go hands me for free.
A Genuinely Nil Interface Panics on Any Method Call
var w io.Writerw.Write([]byte("hello")) // panic: invalid memory address or nil pointer dereferencew here has no concrete type and no value at all, so there’s no method table to dispatch through. This is a different problem from a non-nil interface wrapping a nil concrete pointer — that one passes an err != nil check it shouldn’t. This one is the opposite situation: the interface itself is nil, and calling anything on it panics outright. TypeScript’s closest failure mode is calling a method on undefined, which also throws — but for a different reason. undefined has nothing behind it by definition. Go’s nil interface is a legitimate zero value that type-checks fine right up until the moment you actually call a method on it.
Accept Interfaces, Return Structs
func NewClient(t http.RoundTripper) *Client { return &Client{transport: t}}The parameter takes the narrowest interface the function actually needs, which makes it trivial to swap in a mock for tests. The return value is a concrete struct pointer, not an interface, so the caller gets the client’s full API instead of whatever narrow shape was convenient to accept. I kept wanting to make the signature symmetric — interface in, interface out — because that’s the instinct TypeScript trains. Go deliberately breaks that symmetry on purpose.
iota Is Numbering, Not an Enum
type Level int
const ( Debug Level = iota Info Warn Error)This looks close enough to a TypeScript enum that I assumed it worked the same way:
enum Level { Debug, Info, Warn, Error,}iota is just a counter that resets to 0 at the start of each const block and increments one line at a time — Level itself is a plain int with a name. The gap shows up in exhaustiveness: a TypeScript switch over an enum can be checked at compile time (a default case assigned to never will fail to compile if you add a new enum member and forget a branch). Add a fifth Level in Go and every existing switch still compiles, silently missing the new case. If you want that safety in Go, it comes from a linter, not the type system.
Why json.Unmarshal Takes a Pointer
var cfg Configerr := json.Unmarshal(data, &cfg)Unmarshal’s signature is func Unmarshal(data []byte, v any) error — it can’t just return a Config, because it has no way to know what concrete type to build from raw JSON bytes. Instead you allocate the destination yourself and hand over its address, and Unmarshal writes into that memory directly. JSON.parse(text) in JS skips this entirely and hands back a fresh value, because JS objects don’t need a preallocated destination the way a typed Go struct does.
Takeaway
All three of these push the same kind of explicitness onto the caller that TypeScript’s structural types and enums quietly handle for you: Go won’t infer which concrete type you meant, so you say it yourself — with a receiver, a pointer, or a switch you keep exhaustive by hand.