DEV Community

Nitin Bansal
Nitin Bansal

Posted on

Using NEW, instead of MAKE, to create slice

#go

As we know, slices in golang are reference types, and hence, need make to initialize them.

func main() {
    a := make([]int32, 0)
    a = append(a, 10, 20, 30)
    fmt.Println(len(a), cap(a), reflect.ValueOf(a).Kind())
}
>>> 3 4 slice
Enter fullscreen mode Exit fullscreen mode

But, can we use new to make slices? It's meant to be used for value types only. So how can it work here?🤯

Let's see

func main() {
    a := new([]int32)
}
Enter fullscreen mode Exit fullscreen mode

Does this↑ work? Yes. It does. So what is the difference?

The difference is that this gives you a pointer, as is the case anytime you use new. Thus to actually use it, you have to dereference it everytime.

Example:

func main() {
    a := new([]int32)
    *a = append(*a, 10, 20, 30)
    fmt.Println(len(*a), cap(*a), reflect.ValueOf(*a).Kind())
}
>>> 3 4 slice
Enter fullscreen mode Exit fullscreen mode

Another simple change you can do is to dereference it immediately after creation. This will save you the effort of dereferencing it every other time.

func main() {
    a := *new([]int32)
    a = append(a, 10, 20, 30)
    fmt.Println(len(a), cap(a), reflect.ValueOf(a).Kind())
}
>>> 3 4 slice
Enter fullscreen mode Exit fullscreen mode

There's another important concept:

Every time you append to a slice, it gives you a new slice with a different address (though it points to same underlying array)

Let's see this with same example:

func main() {
    a := make([]int32, 0)
    fmt.Printf("%p\n", a)

    a = append(a, 10, 20, 30)
    fmt.Printf("%p\n", a)

    a = append(a, 10, 20, 30)
    fmt.Printf("%p\n", a)

    fmt.Println(len(a), cap(a), reflect.ValueOf(a).Kind())
}
>>> 0x116bea0
>>> 0xc0000220e0
>>> 0xc00001c100
>>> 6 8 slice
Enter fullscreen mode Exit fullscreen mode

You see, all 3 addresses are different.

But, can we keep the addresses same even after append? Well yes!! It's here your NEW approach comes into place. See this:

func main() {
    a := new([]int32)
    fmt.Printf("%p\n", a)

    *a = append(*a, 10, 20, 30)
    fmt.Printf("%p\n", a)

    *a = append(*a, 10, 20, 30)
    fmt.Printf("%p\n", a)

    fmt.Println(len(*a), cap(*a), reflect.ValueOf(*a).Kind())
}
>>> 0xc00011c030
>>> 0xc00011c030
>>> 0xc00011c030
>>> 6 8 slice
Enter fullscreen mode Exit fullscreen mode

You see? All 3 addresses remain same!!!

That's all folks for now🤩

Top comments (10)

Collapse
 
yusufpapurcu profile image
Yusuf Turhan Papurcu

Nice point. We must try it in benchmarks.

Collapse
 
freakynit profile image
Nitin Bansal

Did the benchmarks. Here is the code and the results: gist.github.com/freakynit/69cacf64...

Observation: Eager Dereferenced version using new always beat other two (non-eager-dereferenced and make).

Collapse
 
yusufpapurcu profile image
Yusuf Turhan Papurcu

Did you tested with structs?
I changed statsHolder := make([]Stats, 0, 11664000) to statsHolder := *new([]Stats) but performance diff is 4x. First one works in ~500ms but second one almost take 2 second. I uploaded example benchmark to here.

Thread Thread
 
freakynit profile image
Nitin Bansal

That will be there. Basically the difference is due to pre-allocation of memory vs on-demand allocation and resizing. See my later comment reply: dev.to/freakynit/comment/1k4fk

Thread Thread
 
yusufpapurcu profile image
Yusuf Turhan Papurcu

Oh I understand now. Thanks for answer 👍

Collapse
 
lil5 profile image
Lucian I. Last

This doesn't make sense

The pointer memory address is the same but the slice address will be different on each append with or without it being referenced by a pointer.

You only just added an extra unnecessary pointer for no reason.

Collapse
 
freakynit profile image
Nitin Bansal

Try it out yourself. The slice address remains same.

Collapse
 
nothinux profile image
Taufik Mulyana

i always use var a []int32, whats the different?

Collapse
 
freakynit profile image

With var a []int32, you are just declaring the variable, not allocating any space to underlying array. For example, this won't work: a[0] = 10. It'll throw index out of range error.
But if you use make, you specify the size, and the space is allocated then and there itself.
This becomes relevant of you know earlier how many elements this will hold. In that case you can straight away initialize the underlying array with that much memory, and directly refer using a[0] notation instead of using append method.

The speed difference is significant too. I benchmarked this for 10 million numbers. Using make(pre-allocating) it is 4x faster.

  1. Using append
func main() {
    var a []int32
    var i int32
    start := time.Now()
    for i = 0; i < 10_000_000; i++ {
        a = append(a, i)
    }
    fmt.Println("Time taken", time.Now().Sub(start).Milliseconds())
}
Enter fullscreen mode Exit fullscreen mode
  1. Pre-allocating using make
func main() {
    var a []int32 = make([]int32, 10_000_000)
    var i int32
    start := time.Now()
    for i = 0; i < 10_000_000; i++ {
        a[i] = i
    }
    fmt.Println("Time taken", time.Now().Sub(start).Milliseconds())
}
Enter fullscreen mode Exit fullscreen mode

The first one took on average 84 ms, while second one only 19-22 ms.

Hope it helps...

Collapse
 
nothinux profile image
Taufik Mulyana

wow amazing, great explanantion, thank you!