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
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)
}
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
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
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
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
You see? All 3 addresses remain same!!!
That's all folks for now🤩
Top comments (10)
Nice point. We must try it in benchmarks.
Did the benchmarks. Here is the code and the results: gist.github.com/freakynit/69cacf64...
Observation:
Eager Dereferenced
version usingnew
always beat other two (non-eager-dereferenced
andmake
).Did you tested with structs?
I changed
statsHolder := make([]Stats, 0, 11664000)
tostatsHolder := *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.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
Oh I understand now. Thanks for answer 👍
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.
Try it out yourself. The slice address remains same.
i always use
var a []int32
, whats the different?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 throwindex 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 usingappend
method.The speed difference is significant too. I benchmarked this for 10 million numbers. Using
make
(pre-allocating) it is 4x faster.append
make
The first one took on average 84 ms, while second one only 19-22 ms.
Hope it helps...
wow amazing, great explanantion, thank you!