Hi guys:
I met a problem when I try to understand slice when using as function
parameters. After writing some code, I made a conclusion by myself, but
need some real check and explanation here, with your help.
As far as I know, Golang's function parameters are passed by value, and
slice could be treated as a descriptor, which seems like *{pointer to first
element of underlying array, length of element, capacity of array}*
according to Go Slices: usage and internals
<https://blog.golang.org/go-slices-usage-and-internals>. So, can I say:
*When passing a slice as a function parameter, it is, in fact, passing a
pointer, and two int values (or as a whole struct, or maybe more parameters
to describe the slice, whatever).*
To make it clear using code:
func foo(s []T) {...}
// can this function be treated as following 3-parameter function?
func foo(p Pointer, length, capa int) {...}
// Note: type is not the point here,
// and we just need 3 parameters, while there maybe more
You may wonder why I have this question? Here is where everything started:
func foo(s []int) {
s := []int{1,2,3}
foo(s)
fmt.Println(s)
}
Everyone know it will print [1 2 3], not [1 2 3 100]. But what I am
interested in, is *whether the function call does modify the memory* just
after the tail of element 3, to an int of 100. If my assumption above is
right, then the modification may happen.
So, I made some experiments.
package main
import "fmt"
import "strconv"
type Node struct {
val int
left *Node
right *Node
}
func (n Node) String () string {
return strconv.Itoa(n.val)
}
func newNode(val int) *Node {
return &Node{val, nil, nil}
}
// Given a binary tree and a target sum, find all root-to-leaf paths
// where each path's sum equals the given sum
func bs(root *Node, path []*Node, now int, target int, ret *[][]*Node) {
if root == nil {
return
}
path = append(path, root)
now += root.val
if now == target && root.left == nil && root.right == nil {
*ret = append(*ret, path)
return
}
bs(root.left, path, now, target, ret)
bs(root.right, path, now, target, ret)
}
func main() {
// a simple tree like:
// 0
// / \
// 1 2
root := newNode(0)
left := newNode(1)
right := newNode(2)
root.left = left
root.right = right
ret := [][]*Node{}
bs(root, make([]*Node, 0, 10), 0, 1, &ret)
fmt.Println(ret)
}
As the code above, it is a function to find all root-to-leaf paths where
each path's sum equals the given sum in a binary tree, and, I make a simple
test case which there are only 3 nodes: one root, two children, with values
of 0, 1 and 2.
Say, I want to find the paths of sum of 1. So, I call this function as:
bs(root, make([]*Node, 0, 10), 0, 1, &ret)
It is petty common to make a guess that, this call will give us a final
result of [0, 1], which is obviously the correct answer, however, it gives
us [0, 2], try yourself if you don't believe:
https://play.golang.org/p/hSKIOaVK2S
The algorithm here is correct, don't worry about it. Instead, please pay
attention to the second parameter of bs(). It makes a slice which has no
element in it, and has a capacity of 10. What if we change this parameter
to:
bs(root, make([]*Node, 0, 1), 0, 1, &ret)
Yes, just make the slice's capacity as 1. This time you will get the right
answer, [0, 1]. Try yourself if you are interested.
Here is my understanding of this strange problem, feel free to point out
anything wrong:
Still the simplest code:
package main
import (
"fmt"
)
func foo(s []int) {
fmt.Println(len(s), cap(s)) // 3 3
s = append(s, 100)
fmt.Println(len(s), cap(s)) // 4 8
}
func main() {
s := []int{1,2,3}
foo(s)
fmt.Println(len(s), cap(s)) // 3 3
fmt.Println(s) // [1 2 3]
}
When the function foo() called, it has 3 parameters: a pointer of the first
element of the array, which points to the element 1, an int for array
length: 3, an int for array capacity: 3. In foo(), it tries to append 100
at the tail, but there is no room for it, according to the parameter of
capacity it receive, so, it makes a larger array, which capacity is 8, then
do some copy-and-append job. Unfortunately, all the work it does is useless
because all three parameters is still themselves. So, at last, it prints [1
2 3]
But, what will happen if the slice passed into foo() is large enough
already?
package main
import (
"fmt"
)
func foo(s []int) {
fmt.Println(len(s), cap(s)) // 3 10
s = append(s, 100)
fmt.Println(len(s), cap(s)) // 4 10
}
func main() {
s := make([]int, 3, 10)
s[0], s[1], s[2] = 1, 2, 3
foo(s)
fmt.Println(len(s), cap(s)) // 3 10
fmt.Println(s) // [1 2 3]
}
Now, foo() is lucky, he does not need to allocate any memory, he just
append 100 at the tail, the descriptor now becomes {address somewhere, 4,
10}. However, in main, it is still {address somewhere, 3, 10} on account of
the pass-by-value-calling-rule, *so, we just care about the first 3
elements of the array, and ignore others because the descriptor tells us
to, even though there does exist the 4th one.*
How to prove there is a 4th one? Which is equal to ask, how to prove the
function call modify the passed slice? Check the path-of-sum algorithm
above, it may show something indirectly.
The algorithm starts from the root, 0, then goes to his left child, 1,
there it meets the target sum, 1 (0+1), so it append 1 into the path slice
(which now is [0,1], descriptor as {address, 2, 10}) and append the path
slice into ret. After this, it will find no child exists, so it returns
from current position and goes back to the root to check root's right
child. Now, the path slice's descriptor is {address, 1, 10}. When checking
the right child, it append 2 into path, which does modify the memory where
1 sits already. At last, it will print [0, 2] as the wrong result.
This tells us one thing: function calls may modify your passed slice, but
the slice descriptor keeps you from knowing it.
My personal explanation is done, and here are my questions:
1. Am I right? What's wrong?
2. If I am right, mostly, how to prevent it gracefully? Any rules, or any
blogs I can learn from?
3. Any better code snippet to do some experiment to make it more
convincing? Does packet reflect <https://golang.org/pkg/reflect/> helpful?
can I just print the 4th element even though the slice descriptor tell me
its length is 3?
4. Where can I find the code of implementation of slice?
Thanks a lot.
--
You received this message because you are subscribed to the Google Groups
"golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/d/optout.