JavaRush /Courses /Go SELF /new(T) vs make in Go are different tools

new(T) vs make in Go are different tools

Go SELF
Level 19, Lesson 2
Available

1. Map of the terrain: what new and make return

If you’re a beginner, a very honest reaction to new and make is: “Why couldn’t they keep one keyword and delete the other with rm -rf?” I get it. But Go has a very grown-up reason here: different data types in the language are built differently, and they need different kinds of “creation”. Some only need memory for the zero value—and they’re ready to work. Others also need their internal structure initialized; otherwise they’re like a box with no bottom: the shape is there, but you can’t use it.

And here’s the key idea: new(T) and make(...) solve different problems, so they don’t replace each other.

Before we go deeper, it helps to have a “map of the terrain” so we don’t get lost. Let’s lock it in as a table, because tables are just lists pretending to be civilized.

Tool What it does What it returns What types it’s typically used for
new(T)
Allocates space for the zero value of type T
*T
For any T when you need a pointer
make(...)
Creates a ready-to-use value with internal state
T
For slice, map, chan

This difference is the main idea of the lecture. Everything else below is detail that will make that idea feel natural.

2. new(T): “give me a pointer to the zero value”

When you write new(T), Go does something very simple: it allocates memory for a value of type T, writes the zero value there, and returns the address of that memory location—that is, *T.

It’s important to feel the wording: new returns a pointer. Not a value, but an address. That’s why new(int) is *int, new(string) is *string, new(Task) is *Task, and so on.

Mini example: new(int) and dereferencing

package main

import "fmt"

func main() {
	p := new(int)    // p: *int, contains 0 inside
	fmt.Println(*p)  // 0
	*p = 10
	fmt.Println(*p)  // 10
}

Here, new(int) gives us an address where 0 is stored. We change *p, and now 10 is stored at that address.

new with a struct: new(struct)

Yes, we’ll cover structs (struct) in detail separately, but today we need the most minimal version: a struct is simply a “value with fields”. No methods, no tags, no drama.

package main

import "fmt"

type Task struct {
	Title string
	Done  bool
}

func main() {
	t := new(Task)               // t: *Task, fields = zero values
	fmt.Println(t.Title, t.Done) // "" false
}

For string the zero value is the empty string; for bool it’s false. So a task freshly created via new(Task) looks like “an empty task and not done yet”. Makes sense.

4. make: “build me a working mechanism”

Unlike new, make is not about “just allocating memory”. make is for types that have internal state that must be correctly initialized.

The classic three types that have make:

  • []T (slice),
  • map[K]V,
  • chan T.

You already use slices and maps actively. Channels we’ll touch very carefully for now (just enough to understand why they’re mentioned next to make).

By the way, a helpful mental model: a slice internally is not an “array”. It’s a small header struct that stores a pointer to data + length + capacity. That’s why slices behave “by reference” with respect to their underlying data, and why append sometimes changes the backing buffer. This “pointer + len + cap” model is standard and very useful for understanding slice behavior.

make([]T, len, cap): create a slice you can use immediately

A slice can exist without make too (for example, var s []int), but then it will be a nil slice. It’s convenient and safe in many places, but it’s important to understand the difference: make creates a slice with a specified length and/or capacity.

make with length: you can index

package main

import "fmt"

func main() {
	nums := make([]int, 3) // len=3, inside [0 0 0]
	nums[0] = 7
	fmt.Println(nums)      // [7 0 0]
}

If you do make([]int, 3), then nums[0], nums[1], nums[2] are all legal.

make with capacity: convenient for append

package main

import "fmt"

func main() {
	nums := make([]int, 0, 5) // len=0, cap=5
	nums = append(nums, 1)
	nums = append(nums, 2)
	fmt.Println(nums)         // [1 2]
}

This way you’re telling the runtime: “I plan to add elements—don’t make me grow the backing array too often.”

make(map[K]V): create a map you can write to

Maps (map) have a separate important point: you can’t write to a nil map. You can read from it (you’ll get the zero value), but writing causes a runtime crash.

So if you want to create a map “from scratch and write into it”, you need make(map[K]V).

package main

import "fmt"

func main() {
	m := make(map[string]int)
	m["go"] = 25
	fmt.Println(m["go"]) // 25
}

make(chan T): create a channel

Channels are a tool for communication between goroutines. We won’t build concurrency right now, but it’s important to understand mechanically: a channel is created with make. Not with new.

package main

import "fmt"

func main() {
	ch := make(chan int)
	fmt.Printf("%T\n", ch) // chan int
}

We’re not even sending or receiving values—just noting the fact: a channel is its own type, and it’s created with make.

5. Why you can’t swap new and make

This is the section where your brain usually “clicks”. If you feel it—congratulations, you just saved yourself a few hours of future debugging.

Why new([]int) is almost always a weird choice

new([]int) returns *[]int, i.e., a pointer to a slice. And inside that slice you’ll have the zero value—that is, a nil slice.

package main

import "fmt"

func main() {
	ps := new([]int)        // ps: *[]int
	fmt.Println(*ps == nil) // true
	fmt.Println(len(*ps))   // 0
}

You get a pointer to a nil slice. A nil slice is fine by itself, but the question is: why do you need a pointer to it? In most tasks—you don’t. A slice is already small (a header), and it’s convenient to pass and return by value.

Why new(map[string]int) doesn’t replace make(map[string]int)

This is one of the most common traps:

  • new(map[string]int) gives *map[string]int, and inside it is a nil map.
  • You can’t write to a nil map.
package main

import "fmt"

func main() {
	pm := new(map[string]int) // *map[string]int, inside nil
	fmt.Println(*pm == nil)   // true

	// (*pm)["a"] = 1          // don't do this: writing to a nil map => panic
	*pm = make(map[string]int) // now the map is real
	(*pm)["a"] = 1
	fmt.Println((*pm)["a"])   // 1
}

Conclusion: new(map[...]...) can be useful only if you conceptually need a pointer to a map (which is rare). But “just to create a map”—you need make.

6. A pointer or a ready-made data structure

Let’s say it in plain language.

If you want to “create an object and mutate it via a pointer”, you naturally reach for new(T) or &T{...} (we’ll learn to love the second one too). But the meaning is always the same: you need an address, i.e., *T.

If you want to create a collection/mechanism (slice, map, chan) that must be properly initialized internally, you use make.

And this isn’t philosophy. It’s a practical rule that makes code predictable.

Nuance: make returns a value, but it can behave “reference-like”

This point matters so you don’t get tangled in wording. make returns a value of type []T, map[K]V, or chan T. Not a pointer. But the behavior often resembles “by reference” because inside these values there are references to shared state.

For a slice, it’s the header that contains a pointer to the backing array (so changes to elements are often visible outside). For map and chan, internally there are pointers to runtime structures too, so a “copy of a map” still points to the same underlying map.

This doesn’t contradict “in Go everything is passed by value”. It’s just that a value can be a small “handle” that leads to a big shared object.

A quick intuitive cheat sheet

It’s useful to lock in the “intuition”:

new(T) is when you want an address (a pointer) to a value of type T, usually in order to:

  • pass around and mutate the same value from different places,
  • return a “big object” from a function without copying it entirely (copying is a separate topic),
  • be able to represent “no object” with nil (*T can be nil).

make is when you create a container/mechanism that must be usable immediately after creation.

7. Learning app TaskBox: new for tasks, make for storage

To avoid keeping the topic too abstract, let’s embed it in a small piece of an app. Let’s say we have a console TaskBox: it stores tasks in memory, can add a few tasks, and print them.

We’ll do two things:

  • create tasks via new(Task) (because we’ll store pointers to tasks),
  • create collections via make: a slice and a map.

Describe the task

package main

type Task struct {
	ID    int
	Title string
	Done  bool
}

No methods, nothing “complicated”. Just fields.

A task factory via new(Task)

Why a factory? Because it’s more convenient: all initialization in one place, and main won’t turn into a mess.

package main

func NewTask(id int, title string) *Task {
	t := new(Task) // *Task with zero values
	t.ID = id
	t.Title = title
	return t
}

Yes, you could do it differently, but today we specifically care about new.

Storage: make([]*Task, 0, ...) and make(map[int]*Task)

The slice will preserve insertion order, and the map will provide fast lookup by ID.

package main

func main() {
	tasks := make([]*Task, 0, 8)
	byID := make(map[int]*Task)

	_ = tasks
	_ = byID
}

Notice: tasks and byID are already ready to use. You can append to tasks, and you can write to byID like byID[id] = ....

Glue it all together: add a couple tasks and print

Let’s make a short but complete example.

package main

import "fmt"

type Task struct {
	ID    int
	Title string
	Done  bool
}

func NewTask(id int, title string) *Task {
	t := new(Task)
	t.ID = id
	t.Title = title
	return t
}

func main() {
	tasks := make([]*Task, 0, 8)
	byID := make(map[int]*Task)

	t1 := NewTask(1, "Learn new vs make")
	t2 := NewTask(2, "Don't confuse nil map with an empty map")

	tasks = append(tasks, t1, t2)
	byID[t1.ID] = t1
	byID[t2.ID] = t2

	fmt.Println(tasks[0].Title) // Learn new vs make
	fmt.Println(byID[2].Title)  // Don't confuse nil map with an empty map
}

Here’s the key thought: NewTask returns *Task, and we store those pointers both in the slice and in the map. That means if we change a task via the pointer, the changes will be visible through both structures (which is fine if that’s what we intend).

8. Common mistakes

Mistake #1: trying to replace make(map[K]V) with new(map[K]V).
new(map[K]V) returns *map[K]V, and inside it will be a nil map. You can read from it, but writing m[k] = v causes a runtime crash. If you need a working map, use make(map[K]V), not new.

Mistake #2: expecting new([]T) to create a slice of the desired length.
new([]T) creates a pointer to the slice zero value, i.e., to a nil slice of length 0. That’s not the same as make([]T, n). If you plan to index s[i], you need make with length.

Mistake #3: making *map or *[]T “just in case”.
Beginners sometimes add pointers everywhere they see the option. But map and []T are already “reference-like” enough in behavior, and extra pointers only complicate the code: you add nil checks, create more non-obvious coupling, and it becomes harder to understand who is mutating data and where.

Mistake #4: thinking make returns a pointer.
make returns a value of type []T, map[K]V, or chan T. Not *[]T and not *map[K]V. If you start writing *m after make(map[...]...), it means you’ve mixed up the mental model and it’s time to stop, take a breath, and reread the table at the start of the lecture.

Mistake #5: forgetting that map has two different “empty states”: nil and “empty but created”.
var m map[string]int gives a nil map: you can read, you can’t write. m := make(map[string]int) gives an empty but working map: you can both read and write. These states look similar (“there’s nothing there”), but they behave differently—and that’s one of the most common causes of unexpected panics.

1
Task
Go SELF, level 19, lesson 2
Locked
Memory for a number
Memory for a number
1
Task
Go SELF, level 19, lesson 2
Locked
Box of numbers
Box of numbers
1
Task
Go SELF, level 19, lesson 2
Locked
Dictionary hint
Dictionary hint
1
Task
Go SELF, level 19, lesson 2
Locked
Task Catalog
Task Catalog
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION