14 Memory Allocation New or Make, Whom to Use Under What Circumstances

14 Memory Allocation - new or make, Whom to Use Under What Circumstances #

Programs require memory to run, such as creating variables, calling functions, and calculating data. Therefore, when memory is needed, it needs to be allocated. In languages like C/C++, memory is managed by developers themselves and needs to be actively allocated and released. In Go, memory is managed by the language itself, and developers don’t need to do much intervention. They only need to declare variables, and Go will automatically allocate the corresponding memory based on the variable’s type.

The virtual memory space managed by Go programs is divided into two parts: heap memory and stack memory. Stack memory is mainly managed by Go and developers cannot interfere much. Heap memory is where developers can unleash their ability, because most of the program data is allocated on the heap memory, and most of the memory usage of a program also occurs on the heap memory.

Pro Tip: When we talk about Go’s memory garbage collection, it refers to garbage collection for the heap memory.

The declaration and initialization of variables involve memory allocation, such as using the var keyword to declare variables. If variables need to be initialized, the = assignment operator is used. In addition, the built-in functions new and make can also be used. You have already seen these two functions in previous lessons. They have similar functionalities, but you may still be confused. So in this lesson, I will explain their differences and usage scenarios based on memory allocation.

Variables #

A data type, after being declared and initialized, is assigned to a variable, which stores the data required for program execution.

Variable Declaration #

As mentioned in the previous lessons, if you simply want to declare a variable, you can use the var keyword, as shown below:

var s string

This example simply declares a variable s of type string without initializing it, so its value is the zero value of string, which is an empty string ("").

Now let’s try declaring a variable of pointer type as follows:

var sp *string

It also works, but it has not been initialized, so its value is the zero value of *string, which is nil.

Variable Assignment #

Variables can be assigned values using the = operator, which modifies the value of the variable. If you want to initialize a variable when declaring it, there are three ways:

  1. Initialize directly during declaration, such as var s string = "飞雪无情".
  2. Initialize after declaration, such as s = "飞雪无情" (assuming variable s has already been declared).
  3. Use := for simple declaration, such as s := "飞雪无情".

Pro Tip: Variable initialization is also a form of assignment, but it happens at the time of variable declaration, which is the earliest moment. In other words, when you obtain this variable, it has already been assigned a value.

Now let’s assign a value to the variable s in the previous example. The sample code is as follows:

func main() {

   var s string

   s = "张三"

   fmt.Println(s)

}

After running the above code, “张三” (which means “张三” in Chinese) will be printed, indicating that there is no problem with direct assignment when the value type variable is not initialized. But what about pointer type variables?

In the following example code, I declared a pointer type variable sp, and then modified its value to “飞雪无情” (“Feixue Wuqing” in Chinese).

func main() {

   var sp *string

   *sp = "飞雪无情"

   fmt.Println(*sp)

}

When running this code, you will see the following error message:

panic: runtime error: invalid memory address or nil pointer dereference

This is because if a pointer variable of a certain type is not allocated memory, it defaults to the zero value nil. It does not point to any memory, so it cannot be used. Trying to use it forcefully will result in the above nil pointer error.

For value types, even if a variable is only declared without being initialized, the variable will still have allocated memory.

In the following example, I declare a variable s and do not initialize it, but I can obtain its memory address using &s. This is actually done by Go language and can be used directly.

```go
func main() {

   var s string

   fmt.Printf("%p\n", &s)

}

Remember when we talked about concurrency and declared the variable wg using var wg sync.WaitGroup? Now you should understand why we can use it without initialization. This is because sync.WaitGroup is a struct, which is a value type, and Go language automatically allocates memory, so it can be used directly without throwing a nil exception.

Therefore, we can come to the conclusion: If you want to assign a value to a variable, the variable must have allocated memory for it, so that you can operate on this memory and achieve the purpose of assignment.

Tip: In fact, not only assignment operations, but also value retrieval operations on pointer variables will throw a nil exception if memory is not allocated, because there is no memory to operate on.

So a variable must be declared and memory allocated before it can be assigned or initialized when declared. When a pointer type is declared, Go language does not allocate memory automatically, so you cannot assign a value to it, unlike value types.

Tip: The same applies to map and chan types, as they are also pointer types in essence.

The new function #

Since we already know that a declared pointer variable does not have allocated memory by default, we just need to allocate some memory for it. So the hero of today, the new function, comes on stage. For the example above, you can use the new function to refactor it as follows:

func main() {

   var sp *string

   sp = new(string)// Key point

   *sp = "飞雪无情"

   fmt.Println(*sp)

}

The key point in the above code is that a *string is generated using the built-in new function and assigned to the variable sp. Now if you run the program, it will work correctly.

What is the purpose of the built-in function new? We can analyze its source code definition as shown below:

// The new built-in function allocates memory. The first argument is a type,
    
// not a value, and the value returned is a pointer to a newly
    
// allocated zero value of that type.
    
func new(Type) *Type

Its purpose is to allocate a block of memory based on the type passed in, and then return a pointer pointing to that block of memory. The data pointed to by this pointer is the zero value of that type.

For example, if the type passed in is string, then the return value will be a string pointer, and the data pointed to by this string pointer is an empty string, as shown below:

sp1 = new(string)

fmt.Println(*sp1) // Prints an empty string, which is the zero value of string.

After allocating memory using the new function and returning a pointer to that memory, you can perform assignment, retrieval, and other operations on that memory using the pointer.

Variable initialization #

When declaring variables of certain types, their zero values may not meet our requirements. In this case, we need to initialize the variables at the time of declaration (modify their values), which is called variable initialization.

The following example demonstrates the initialization of a variable of type string. Because its zero value (an empty string) does not meet our needs, it needs to be initialized as "飞雪无情" at the time of declaration.

```go
var s string = "飞雪无情"

s1 := "飞雪无情"

Not only basic types can be initialized using the above literal approach, but composite types can also be initialized. For example, the person struct from the previous course example is as follows:

type person struct {
  name string
  age  int
}

func main() {
  // Literal initialization
  p := person{name: "张三", age: 18}
}

In this code example, when declaring the p variable, its name is initialized as “张三” and age is initialized as 18.

Initializing Pointer Variables #

In the previous section, you learned that the new function can allocate memory and return a pointer to that memory, but the data value in that memory is the default zero value for that type, which may not meet business requirements in some cases. For example, I want to obtain a pointer of type *person with name as “飞雪无情” and age as 20, but the new function only has a type parameter and does not have any initialization value parameter. What should I do in this case?

To achieve this, you can customize a function to initialize pointer variables, as shown below:

func NewPerson() *person {
  p := new(person)
  p.name = "飞雪无情"
  p.age = 20
  return p
}

Do you remember the factory function mentioned in the previous course? The NewPerson function in this code example is a factory function. In addition to using the new function to create a person pointer, it also assigns the initial values. By wrapping the memory allocation (new function) and initialization (assignment) in the NewPerson function, the user of the NewPerson function will obtain a *person pointer with name as “飞雪无情” and age as 20. By doing this, the memory allocation (using new function) and initialization (assignment) are both completed.

The following code is an example of using the NewPerson function. It prints the value of the data pointed to by pp to verify whether name is “飞雪无情” and age is 20.

pp := NewPerson()
fmt.Println("name为", pp.name, ",age为", pp.age)

To make the custom factory function NewPerson more universal, let’s modify it to accept name and age as parameters:

pp := NewPerson("飞雪无情", 20)

func NewPerson(name string, age int) *person {
  p := new(person)
  p.name = name
  p.age = age
  return p
}

The effects of this code are the same as the previous example, but the NewPerson function is more universal because you can pass different parameters to construct different *person variables.

The make Function #

After all the preparations, we finally come to the second protagonist of today: the make function. In the previous lesson, you already learned that when creating a map using the make function, it actually calls the makemap function. Here is an example:

m := make(map[string]int, 10)

As you can see, the make function and the custom NewPerson function from the previous section are quite similar. In fact, the make function is a factory function for the map type. It can create maps of different types based on the key-value type passed to it, and it can also initialize the size of the map.

Tip: The make function is not only a factory function for map types, but also for slice and channel types. It can be used to initialize slice, channel, and map types.

Summary #

Through the explanation in this lesson, I believe you now understand the difference between the new and make functions. Now let me summarize.

The new function is only used for memory allocation and initializes the memory to zero value of the corresponding type, which means it returns a pointer to the zero value of that type. The new function is generally used in cases where you explicitly need to return a pointer, but it is not commonly used.

The make function is only used for creating and initializing slice, channel, and map types. These three types have complex internal structures. For example, a slice needs to initialize the type of its internal elements, as well as its length and capacity in advance, in order to use them properly.

At the end of this lesson, I have a exercise for you: use the make function to create a slice and use different lengths and capacities as parameters to see their effects.

In the next lesson, I will introduce “Runtime Reflection: How to Convert Between Strings and Structs?” Be sure to listen to the lesson!