Go Explored: new() vs make()

Go, Golang

The Go standard library includes two built-in functions that are commonly used, though in many cases are not totally clear about what they are doing behind the scenes and why they are being used. They are new() and make().

As a side note: this blog post is more of a reminder to myself about what these do.

The new() function

The new() function can instantiate any struct or value data type. When new is called, Go will allocate the memory without initializing it. This means that the memory reserved is "zeroed". When new is called with a data type it will return a pointer to a zero value memory location of the type you've specified.

Here's a simple example:

i := new(int)
fmt.Println(*i)
// prints: 0

In the above example new(int) reserves space for an integer, initializes that space to zero, and returns a pointer to it. Like most Go variables, the space is released by the garbage collector when the pointer is no longer referenced.

Example

new() would be used when you want to allocated a zeroed value for the pointer it creates. This is useful when working with basic types or custom structs.

type Person struct {
    Name string
    Age  int
}

p := new(Person)
fmt.Println(*p)
// prints: { 0}

The make() function

make() has a much tighter scope as it can only instantiate channels, maps, and slices and returns an initialized value (not a pointer) which is not zeroed. The data structure of the type is ready for use.

Here's a simple example:

s := make([]int, 5)
fmt.Println(s)
// prints: [0 0 0 0 0]

In the above example make([]int, 5) creates a slice of integers with a length of 5 where all elements within are initialized to zero.

Example

make() is used when you're want to prepare a channel, map, or slice. This is optimal in situations where you know the starting amount of resources you'll be inserting into these datastructurs. This removes the need for reallocated when setting up the contents of a structure.

// channel example
ch := make(chan int)
go func() { ch <- 1 }()
fmt.Println(<-ch)
// prints: 1

// map example
m := make(map[string]int)
m["one"] = 1
fmt.Println(m) // prints: map[one:1]

// slice example
s := make([]int, 10)
fmt.Println(s) // prints: [0 0 0 0 0 0 0 0 0 0]

In all three of the above examples, the data structure is created and ready for immediate use. In the case of the slice example, there are 10 spots already allocated and ready for data.

Instantiating

Both of these built-in's shouldn't (necessarily) be the go-to methods for creating their respective types. There are specific contexts when it makes sense to use them.

new()

Using new() is great when you need a pointer and that's all. If you already have a value (eg. an int) to be assigned then you could just create that i := 1 and if reference the pointer like p := &i. A common use case for creating a pointer is when functions take a pointer and alter the underlying data.

func main() {
	year := new(int)

	if err := GetYear(year); err != nil {
		panic(err)
	}

	fmt.Println("The year is:", *year)
}

func GetYear(i *int) error {
	if *i != 0 {
		return fmt.Errorf("expected 0 value")
	}

	*i = time.Now().Year()
	return nil
}

When using new(), &, or another style of instantiating, you should review the specific requirements of your code given it's context and pick the clear and concise form.

make()

make() is very powerful when you have an idea of what kind of space and records your structure will require. For example, a program may have a set of 5 values coming in that it wants to put in a slice, and that slice will likely grow to 15 total values. The slice can be prepped like s := make([]int, 5, 15).

The 5 represents the amount of zeroed values that will be included in the slice. The 15 is the allocated space that will be available reserved for that slice. This means that up to 10 more values can be added before the slice space needs to be reallocated and expanded.

Looping over this slice would look like:

for _, i := range s {
	fmt.Print(i, ",")
}
// prints 0,0,0,0,0,

Note, that it only prints five 0's. This is because those have been instantiated. Some more examples:

var s1 []int                // length and capacity are 0
s2 := []int{}               // length and capacity are 0
s3 := []int{1, 2, 3}        // length and capacity are 3
s4 := make([]int, 3)        // length and capacity are 3
s5 := make([]int, 3, 100)   // length is 3, capacity is 100

In the above examples length is the amount of instantiated, zero values, and capacity is the amount of spaces resevered in memory.

Conclusion

Understanding how and when to use Go's built-in functions new() and make() can make a substantial difference in how you handle memory allocation and initialization in your Go programs.

new() and make() serve different purposes. new(T) is used to allocate zeroed memory for a variable of type T and returns a pointer to it, useful for all types. This is particularly helpful when you need to obtain a pointer of a zero value of any data type, such as basic types or custom structs.

make(), on the other hand, is designed to create complex data types like slices, maps, and channels with an underlying structure that's ready for use. It allows you to initialize these types and specify their capacity right off the bat, saving you from the costly process of memory reallocation when the data structures need to grow.

But remember, although new() and make() are powerful, they are not always the best tools for the job. Regular variable declaration and initialization using var or := is often clearer and more idiomatic. In case of new(), if you already have a value to assign to the variable, you might not need new(). Instead, you can use := to declare and initialize the variable, then & to get its address if necessary.

In the case of make(), while it's great when you have a clear idea of the capacity your data structures require, for small data structures or if you don't have a capacity in mind, using slice literals or declaring a map or channel directly can be simpler and clearer.

At the end of the day, the key to writing efficient, clear, and idiomatic Go code is to understand these tools and choose the right one for your specific use case. Happy coding!