通过参考或价值

if i had an instance of the following struct

type Node struct {
    id          string 
    name        string  
    address     string
    conn        net.Conn
    enc         json.Encoder
    dec         json.Decoder
    in          chan *Command
    out         chan *Command
    clients     map[string]ClientNodesContainer
}

i am failing to understand when i should send a struct by reference and when should i send it by value(considering that i do not want to make any changes to that instance), is there a rule of thumb that makes it easier to decide?

all i could find is send a struct by value when its small or inexpensive to copy, but does small really mean smaller than 64bit address for example?

would be glad if someone can point some more obvious rules

The rule is very simple:

There is no concept of "pass/send by reference" in Go, all you can do is pass by value.

Regarding the question whether to pass the value of your struct or a pointer to your struct (this is not call by reference!):

  1. If you want to modify the value inside the function or method: Pass a pointer.
  2. If you do not want to modify the value:
    1. If your struct is large: Use pointer.
    2. Otherwise: It just doesn't matter.

All this thinking about how much a copy costs you is wasted time. Copies are cheap, even for medium sized structs. Passing a pointer might be a suitable optimization after profiling.

Your struct is not large. A large struct contains fields like wholeWorldBuf [1000000]uint64. Tiny structs like yours might or might not benefit from passing a pointer and anybody who gives advice which one is better is lying: It all depends on your code and call patterns.

If you run out of sensible options and profiling shows that time is spent copying your structs: Experiment with pointers.

Most of the time you should use passing by reference. Like:

func (n *Node) exampleFunc() {
    ...
}

Only situation when you would like to use passing instance by value is when you would like to be sure that your instance is safe from changes.

The principle of "usually pass values for small structs you don't intend to mutate" I agree with, but this struct, right now, is 688 bytes on x64, most of those in the embedded json structs. The breakdown is:

  • 16*4=64 for the three strings (pointer/len pairs) and the net.Conn (an interface value)
  • 208 for the embedded json.Encoder
  • 392 for the embedded json.Decoder
  • 8*3=24 for the three chan/map values (must be pointers)

Here's the code I used to get that, though you need to save it locally to run it because it uses unsafe.Sizeof.

You could embed *Encoder/*Decoder instead of pointers, leaving you at 104 bytes. I think it's reasonable to keep as-is and pass *Nodes around, though.

The Go code review comments on receiver type say "How large is large? Assume it's equivalent to passing all its elements as arguments to the method. If that feels too large, it's also too large for the receiver." There is room for differences of opinion here, but for me, nine values, some multiple words, "feels large" even before getting precise numbers.

In the "Pass Values" section the review comments doc says "This advice does not apply to large structs, or even small structs that might grow." It doesn't say that the same standard for "large" applies to normal parameters as to receivers, but I think that's a reasonable approach.

Part of the subtlety of determining largeness, alluded to in the code review doc, is that many things in Go are internally pointers or small, reference-containing structs: slices (but not fixed-size arrays), strings, interface values, function values, channels, maps. Your struct may own a []byte pointing to a 64KB buffer, or a big struct via an interface, but still be cheap to copy. I wrote some about this in another answer, but nothing I said there prevents one from having to make some less-than-scientific judgement calls.

It's interesting to see what standard library methods do. bytes.Replace has ten words' worth of args, so that's at least sometimes OK to copy onto the stack. On the other hand go/ast tends to pass around references (either * pointers or interface values) even for non-mutating methods on its not-huge structs. Conclusion: it can be hard to reduce it to a couple simple rules. :)