Allocation

September 19, 2024

Today’s topic: Allocation!

What? You thought allocation was relegated to languages like C and C++?

Yeah, that’s actually pretty close to true. We’re not learning about malloc today. Rather, we’re learning about the built-in function new, which gives us just one (of many!) ways to allocate memory for a variable.

Allocation

The built-in function new takes a type T, allocates storage for a variable of that type at run time, and returns a value of type *T pointing to it. The variable is initialized as described in the section on initial values.

new(T)

For instance

type S struct { a int; b float64 }
new(S)

allocates storage for a variable of type S, initializes it (a=0, b=0.0), and returns a value of type *S containing the address of the location.

A natural, and expected, question after reading that, would be: Okay, but when would I use new, and why?

Before I answer that, let’s look at some equivalents and near equivalents.

type S struct { a int; b float64 }
x1 := &S{}
x2 := new(S)
var y *S
var z S
x3 := &z

In this code, x1, x2, and x3 are equivalent. They’re all pointers to an instance of the zero value of S. (They aren’t equal, in that they all point to distinct instances, so their pointer values are different.)

Of note, y, is not the same. It is of the same type, but it is actually a nil pointer of type *S.

So now then, let’s get back to when and why to use new as opposed to these alternatives.

First, var z S; x3 := &z is a rather cumbersome way to initialize x3, so it quickly falls out of the running. That leaves &S{} and new(S). Which to choose?

This mostly boils down to preference, in the majority of cases. My preferences is usually for the &S{} syntax. Most important, it’s easier to expand, if I later decide to initialize any of its members to a non-zero value:

x1 := &S{a: 3}

And that relates to the second point I’ll make, and when I might choose new as an alternative: If I want to convey semantically that the value I’m creating is explicitly a pointer to the zero value, I might choose new. It’s rare that I want to do this. It’s rare that I use new. Except…

The one exception to all this, is the one time when new is the only good option. When is that?

When you’re trying to create a new value within a generic function.

func Foo[T any]() (T, error) {
	/* Some logic */
	if err != nil {
		return nil, err
	}
	/* more logic */
}

This above code won’t work, because nil is not a valid value for all members of T. T might be a string or int, which cannot be nil. So how do we return the correct zero value of T?

Technically, there are options, but the most concise one is:

		return *new(T), err

Quotes from The Go Programming Language Specification Language version go1.23 (June 13, 2024)


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Length and capacity

Length and capacity The built-in functions len and cap take arguments of various types and return a result of type int. The implementation guarantees that the result always fits into an int. Recall that int is either a 32- or 64-bit integer. So this means that the theoretical maximum length or capacity of the various types that support len and cap depends on the CPU architecture. However, this should not matter in practice, since you’d quickly exceed the available memory, before you had a slice, array, map, or other item with 2^32 elements in it.


Type switches with generics

More live coding today! Join me! And bring your questions. Type switches … A type parameter or a generic type may be used as a type in a case. If upon instantiation that type turns out to duplicate another entry in the switch, the first matching case is chosen. func f[P any](x any) int { switch x.(type) { case P: return 0 case string: return 1 case []P: return 2 case []byte: return 3 default: return 4 } } var v1 = f[string]("foo") // v1 == 0 var v2 = f[byte]([]byte{}) // v2 == 2 The example included is nice, because it shows two examples of the type parameter P being used: Both the first case (case P) and third (case []P) use the type parameter.


Type switching on a non-interface value. Sorta.

Type switches … Cases then match actual types T against the dynamic type of the expression x. As with type assertions, x must be of interface type, but not a type parameter, and each non-interface type T listed in a case must implement the type of x. The types listed in the cases of a type switch must all be different. TypeSwitchStmt = "switch" [ SimpleStmt ";" ] TypeSwitchGuard "{" { TypeCaseClause } "}" .

Get daily content like this in your inbox!

Subscribe