Preferred way to instantiate types in Go
Asked Answered
S

5

13

I like the fact that Go doesn't give me a million ways to do simple things – to borrow from The Zen of Python, “There should be one – and preferably only one – obvious way to do it.”

However, I'm not clear on the preferred/idiomatic way of instantiating types. The basic types are easy:

n := 0
t := 1.5
str := "Hello"

But what about structs? Are the following equivalent, and if so, which is preferred and why?

var f Foo    
f := Foo{}

What about slices? I can do var xs []int, xs := []int{}, or xs := make([]int), but I think the first option (as opposed to with structs) is different from the others? I assume this will also apply to maps.

With pointers, I hear that new should be avoided. Is this good advice, and if so, what would count as a valid usage of new?

I realize that this may partly be a question of style, but a rationale for preferring a particular style would be helpful in any case.

Shimmery answered 11/7, 2013 at 23:48 Comment(0)
A
14

When you declare a variable, where T is some type:

var name T

Go gives you a piece of uninitialized "zeroed" memory.

With primitives, this means that var name int would be 0, and var name string would be "". In C it might be zeroed, or might be something unexpected. Go guarantees an uninitialized variable is the type's zero equivalent.

Internally slices, maps, and channels are treated as pointers. Pointers zero value is nil, meaning it points to nil memory. Without initializing it, you can encounter a panic if you try to operate on it.

The make function is specifically designed for a slice, map, or channel. The make function's arguments are:

make(T type, length int[, capacity int]) // For slices.
make(T[, capacity int]) // For a map.
make(T[, bufferSize int]) // For a channel. How many items can you take without blocking?

A slices length is how many items it starts with. The capacity is the allocated memory before a resize is needed (internally, new size * 2, then copy). For more information see Effective Go: Allocation with make.

Structs: new(T) is equivalent to &T{}, not T{}. *new(T) is equivalent to *&T{}.

Slices: make([]T,0) is equivalent to []T{}.

Maps: make(map[T]T) is equivalent to map[T]T{}.

As far as which method is preferred, I ask myself the following question:

Do I know the value(s) right now inside the function?

If the answer is "yes", then I go with one of the above T{...}. If the answer is "no", then I use make or new.

For example, I would avoid something like this:

type Name struct {
    FirstName string
    LastName string
}

func main() {
    name := &Name{FirstName:"John"}
    // other code...
    name.LastName = "Doe"
}

Instead I would do something like this:

func main() {
    name := new(Name)
    name.FirstName = "John"
    // other code...
    name.LastName = "Doe"
}

Why? Because by using new(Name) I make it clear that I intend to fill the values later. If I used &Name{...} it wouldn't be clear that I intended to add/change a value later in the same function without reading the rest of the code.

The exception is with structs when you don't want a pointer. I'll use T{}, but I won't put anything in it if I plan to add/change the values. Of course *new(T) also works, but that's like using *&T{}. T{} is cleaner in that case, although I tend to use pointers with structs to avoid making a copy when passing it around.

Another thing to keep in mind, a []*struct is smaller and cheaper to resize than []struct, assuming the struct is much larger than a pointer, which is typically 4 - 8 bytes (8 bytes on 64bit?).

Abdu answered 12/7, 2013 at 19:3 Comment(0)
G
5

During the Fireside Chat with the Go Team at Google IO, someone in the audience asked the Go team what they would like to take out from the language.

Rob said he wished there was less way to declare variables and mentioned:

Colon equals for overwrite, named result parameters ( https://plus.google.com/+AndrewGerrand/posts/LmnDfgehorU), variable reused in a for loop being confusing, especially for closures. However the language is probably not going to change much.

Grantley answered 12/7, 2013 at 8:39 Comment(1)
+1 Doesn't really answer the question but I sure do agree - glad to know that Pike himself said so. That is one weakness I'm finding with GoLang: too many ways to declare and not clear the benefits and drawbacks and appropriateness of them each - gives me a sort of "not quite finished" feeling sometimes.Buncombe
G
4

You could have a look at the Go standard library sources where you can find lot of idiomatic Go code.

You are right: var xs []int differs from the other two variants as it does not "initialize" xs, xs is nil. While the other two really construct a slice. xs := []int{} is common if you need an empty slice with zero cap while make gives you more options: length and capacity. On the other hand it is common to start with a nil slice and fill by appending as in var s []int; for ... { s = append(s, num) }.

new cannot be avoided total as it is the only way to create a pointer e.g. to uint32 or the other builtin types. But you are right, writing a := new(A) is pretty uncommon and written mostly as a := &A{} as this can be turned into a := &A{n: 17, whatever: "foo"}. Usage of new is not really discouraged, but given the ability of struct literals it just looks like a leftover from Java to me.

Gilbertina answered 12/7, 2013 at 0:20 Comment(0)
C
0

slice

  1. var xs []int
  2. xs := []int{}
  3. xs := make([]int, 2)

I avoid third item, unless I need to declare a size:

xs := make([]int, 2)
xs[1] = 100

I avoid second item, unless I have values to include:

xs := []int{9, 8}

map

  1. xs := make(map[string]int)
  2. xs := map[string]int{}

I avoid second item, unless I have values to include:

xs := map[string]int{"month": 12, "day": 31}

struct

  1. var f Foo
  2. f := Foo{}

I avoid second item, unless I have values to include:

f := Foo{31}
f := Foo{Day: 31}

pointer

  1. var f Foo; &f
  2. f := new(Foo)
  3. f := &Foo{}

I avoid third item, unless I have values to include:

f := &Foo{31}
f := &Foo{Day: 31}

I avoid second item, unless every use of the variable will be in "pointer mode":

m, b := map[string]int{"month": 12, "day": 31}, new(bytes.Buffer)
json.NewEncoder(b).Encode(m)
http.Post("https://stackoverflow.com", "application/json", b)
Calcium answered 17/4, 2021 at 14:5 Comment(0)
C
0

There is a lot of misuse of the term initialized in this thread. Values that are guaranteed to be nil, 0, or "" are all initialized, not uninitialized. Values that may have random leftover memory content in them are uninitialized. C does not initialize memory, Go does.

Claret answered 26/5, 2023 at 19:27 Comment(1)
This does not provide an answer to the question. Once you have sufficient reputation you will be able to comment on any post; instead, provide answers that don't require clarification from the asker. - From ReviewSaundra

© 2022 - 2024 — McMap. All rights reserved.