I see from your comment that you are still not sure what is going on here:
I want to allocate an array once and then reuse it (clearing before use, of course) in iterations of a for loop. Do you mean that arr = [3]int{} does reallocation as opposed to clearing with a for i := range arr {arr[i] = 0}?
First, let's toss arrays entirely out of the question. Suppose we have this loop:
for i := 0; i < 10; i++ {
v := 3
// ... code that uses v, but never shows &v
}
Does this create a new variable v on each trip through the loop, or does this create one variable v outside the loop and on each trip through the loop, stick 3 into the variable at the top of the loop? The language proper does not have an answer to this question for you. The language describes the behavior of the program, which is that v is initialized to 3 each time, but if we never observe &v, perhaps &v is the same each time.
If we choose to observe &v, and actually run this program in some implementation, we'll see the implementation's answer. Now things get interesting. The language says that each v—each one allocated in a new trip through the loop—is independent of any previous v. If we take &v, we have at least a potential ability for each instance of v to still be live on a subsequent trip through the loop. So each v must not interfere with any previous variable v. The easy way for the compiler to guarantee this to allocate it anew each time, using reallocation.
The current Go compilers use escape analysis to try to detect whether some variable's address is taken. If so, that variable is heap-allocated rather than stack-allocated, and the runtime system relies on the (runtime) garbage collector to free the variable. We can demonstrate this with a simple program on the Go playground:
package main
import (
"fmt"
"runtime"
)
func main() {
for i := 0; i < 10; i++ {
if i > 5 {
runtime.GC()
}
v := 3
fmt.Printf("v is at %p\n", &v)
}
}
This program's output is not guaranteed to look like this, but here is what I get when running it:
v is at 0xc00002c008
v is at 0xc00002c048
v is at 0xc00002c050
v is at 0xc00002c058
v is at 0xc00002c060
v is at 0xc00002c068
v is at 0xc000094040
v is at 0xc000094050
v is at 0xc000094040
v is at 0xc000094050
Note how v's addresses start coinciding (albeit alternating) once we force the garbage collector to start running, when i takes on values 6 through 10. That's because v really does get allocated anew each time, but by having the GC run, we make some previously-allocated, no-longer-in-use memory available again. (Precisely why this alternates is a bit of a mystery, but the behavior is likely to depend on many factors, such as the Go version, runtime startup allocations, how many threads your system is willing to use, and so on.)
What we've shown here is that Go's escape analysis thinks that v escaped, so it allocates a new one each time. We passed &v to fmt.Printf, which is what let it escape. A future compiler might be smarter: it might know that fmt.Printf does not save the value of &v, so that the variable is dead after fmt.Printf returns, and did not actually escape; in that case, it might re-use &v each time. But as soon as we add something where that would matter in an important way, v really would escape and the compiler would have to go back to allocating each one separately.
The key question is observability
Unless you take the address of a variable—your entire array or any of its elements, for instance—in Go, the only thing you can observe about the thing is its type and value. In general, means you can't tell if the compiler has made a new copy of some variable, or reused an old copy.
If you pass an array to a function, Go passes the entire array by value. This means that the function cannot change the original values in the array. We can see how this is observable by writing a function that does in fact change the values:
package main
import (
"fmt"
)
func observe(arr [3]int) {
fmt.Printf("at start: arr = %v\n", arr)
for i, v := range arr {
arr[i] = v * 2
}
fmt.Printf("at end: arr = %v\n", arr)
}
func main() {
a := [3]int{1, 2, 3}
for i := 0; i < 3; i++ {
observe(a)
}
}
(playground link).
Arrays in Go are passed by value, so this does not change the array a in main even though it does change the array arr in observe.
Often, though, we'd like to change the array and preserve these changes. To do that, we must either:
Now we can see the values change, with those changes preserved across function calls, even if we never look at the various addresses. The language says that we must be able to see these changes, so we can; the language says that we must not be able to see the changes when we pass the array itself by value, so we can't. It's up to the compiler to come up with some way of making this happen. Whether that involves copying the original array, or some other mystery magic, is up to the compiler—though Go tries to be a straightforward language where the "magic" is obvious, and the obvious way is to copy, or not copy, as appropriate.
The point of all this
Besides worrying about observable effects—i.e., whether we're computing the right answer in the first place—the point of doing all of these experiments is to show that the compiler could do whatever it wants to, as long as it produces the right observable effects.
You can attempt to make things easier for the compiler, e.g., by allocating a single array, using it by address (&a in the example above) or slice (a[:] in the example above), and clearing it yourself.1 But this might not be any faster, and might even be slower, than just writing it however you find clearest. Write it the clear way first, then time it. If it's too slow, try assisting the compiler, and time it again. Your assist might make things worse or have no effect: if so, don't bother. If it makes things much better, keep it.
1Knowing that the compiler you're using does escape analysis, if you want to help it out, you can run it with the flags that make it tell you which variables have escaped. These are often opportunities for optimization: if you can figure out a way to keep the variable from escaping, the compiler can allocate it on the stack, rather than the heap, which may save a useful amount of time. But if your time isn't really being spent in the allocator in the first place, this won't actually help anyway, so profiling is usually the first step.