5

Taking the standard library net/http for example, the DefaultClient is defined as:

var DefaultClient = &Client{}

However, the DefaultServeMux is defined as:

var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

What's the point of defining two variables for one object? What benefit does it bring over a simple var DefaultServeMux = &ServeMux{}?

iBug
  • 35,554
  • 7
  • 89
  • 134
  • 1
    [This](https://go-review.googlesource.com/c/go/+/20765) is related and _may_ be of interest to you, though I'm not sure the optimization would be "lost" if `var x = &Foo{}` was used instead. Unrelated to optimization, this pattern is used to export otherwise unexported identifiers for tests that declare a separate test package. – mkopriva Apr 07 '23 at 06:44
  • 1
    As an example of the pattern used to export identifiers for tests see [this](https://github.com/golang/go/blob/master/src/reflect/export_test.go#L24), it makes the unexported identifier available to [this](https://github.com/golang/go/blob/master/src/reflect/abi_test.go#L243-L247) (notice that the file declares a separate test package, i.e. `reflect_test`, which makes unexported identifiers declared in `reflect` unavailable to it) – mkopriva Apr 07 '23 at 06:48

1 Answers1

3

The DefaultServeMux part is an optimization done by Brad Fitzpatrick. I will copy and reformat the conversations between him and Matthew Dempsky from the comments on the CL:

Matthew Dempsky:

To make sure I understand, the problem is that doing

var x = newFoo()

requires newFoo() to be called even if x is unused

Brad Fitzpatrick:

Yes, that generates:

var x *T

func init() {
  x = newFoo()
}

And the linker doesn't seem to ever remove init blocks, even if they only assign to things which are otherwise only ever read. Because maybe newFoo has side-effects too?

Matthew Dempsky:

whereas

var x = &y
var y foo

skips the explicit initialization code. So if x is otherwise unused, it can be dead code eliminated along with all of type foo's related code?

Brad Fitzpatrick:

Yup


I have checked the size of the go binary compiled from the latest source code (commit 4f4a9c7fff), and I found that the change of that commit helps a lot, but var DefaultServeMux = &ServeMux{} is a little better. See the table below:

Implementation size of the go binary
var DefaultServeMux = NewServeMux() 15870476
var DefaultServeMux = &defaultServeMux; var defaultServeMux ServeMux 15864704 (-5772)
var DefaultServeMux = &ServeMux{} 15864696 (-5780)

Update:

I tried to find out the difference between var DefaultServeMux = &defaultServeMux; var defaultServeMux ServeMux and var DefaultServeMux = &ServeMux{} at the time when that commit was created (it's back to go1.7). But I failed to compile go1.12 on my machine, so I gave up (see https://stackoverflow.com/a/76030876/1369400). What I have found is from go1.7 till now (devel go1.21-4f4a9c7fff), the two implementations are almost the same regarding the affect to the size of the go binary. Here is the result (my environment is go1.20.3 linux/amd64 and I compiled the binary with ./make.bash):

git tag var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
var DefaultServeMux = &ServeMux{} delta
go1.7 9966320 9966320 0
go1.8 10081258 10081258 0
go1.9 10381836 10381836 0
go1.10 11320890 11320890 0
go1.11 13032188 13032188 0
go1.12 14576719 14576719 0
go1.13 15083862 15083862 0
go1.14 15323624 15323624 0
go1.15 14272775 14272775 0
go1.16 14068974 14068982 +8
go1.17 14022237 14022237 0
go1.18 14545517 14545517 0
go1.19 15302909 15302909 0
go1.20 15579106 15579106 0
4f4a9c7fff 15864704 15864696 -8
Zeke Lu
  • 6,349
  • 1
  • 17
  • 23