This is a creation in Article, where the information may have evolved or changed.
Recently saw a video on the tubing: Understanding Nil, very interesting, this article on the video to do a summary, code samples are from the video.
What's nil?
I believe that the programmer who wrote Golang is very familiar with the following code:
if err != nil { // do something....}
When there is a non nil
-equal, it means that some error occurred, we need to do some processing of this error, and if it is equal to the instructions to nil
run normally. Then what is it? nil
Check the dictionary to see if it nil
means nothing, or a value of 0. 0 value, zero value, isn't it a bit familiar? In the go language, if you declare a variable but do not assign it, the variable will have a default value of 0 for the type. This is the corresponding 0 value for each type:
bool -> false numbers -> 0 string -> "" pointers -> nilslices -> nilmaps -> nilchannels -> nilfunctions -> nilinterfaces -> nil
As an example, when you define a struct:
type Person struct { AgeYears int Name string Friends []Person}var p Person // Person{0, "", nil}
Variables are p
declared but not assigned, so all fields of P have a corresponding value of 0. So, nil
what exactly is this? In the Go document,Nil is a predefined identifier that represents the 0 value of a pointer, channel, function, interface, map, or slice , which is a predefined variable:
type Type intvar nil Type
Not a little surprised? nil
not one of the key words of go, you can even change nil
the value yourself:
var nil = errors.New("hi")
This can be completely compiled, but it's best not to do it this way.
What's the use of nil?
After knowing what's going on nil
, then say what's the nil
use.
Pointers
var p *intp == nil // true*p // panic: invalid memory address or nil pointer dereference
The pointer represents the address that points to memory, which causes panic if the pointer to nil is dereferenced. So nil
what's the use of pointers for? Let's look at an example of a binary tree calculation:
type tree struct { v int l *tree r *tree}// first solutionfunc (t *tree) Sum() int { sum := t.v if t.l != nil { sum += t.l.Sum() } if t.r != nil { sum += t.r.Sum() } return sum}
The code above has two problems, one is code duplication:
if v != nil { v.m()}
The other is when t
it's nil
time to panic:
var t *treesum := t.Sum() // panic: invalid memory address or nil pointer dereference
How to solve the above problem? Let's take a look at an example of a pointer receiver:
type person struct {}func sayHi(p *person) { fmt.Println("hi") }func (p *person) sayHi() { fmt.Println("hi") }var p *personp.sayHi() // hi
For the method of pointer object, even if the value of the pointer is nil
also can be called, based on this, we can just calculate the binary tree and the example of the transformation:
func(t *tree) Sum() int { if t == nil { return 0 } return t.v + t.l.Sum() + t.r.Sum()}
Is it a lot simpler to compare with the code just now? For nil
pointers, it's OK to just judge the method in front of it, without repeating the judgment. Change to the value of the print binary tree or find a value for the binary tree is the same:
func(t *tree) String() string { if t == nil { return "" } return fmt.Sprint(t.l, t.v, t.r)}// nil receivers are useful: Findfunc (t *tree) Find(v int) bool { if t == nil { return false } return t.v == v || t.l.Find(v) || t.r.Find(v)}
So if it's not very necessary, don't use NEWX () to initialize the values, but instead use their default values.
Slices
// nil slicesvar s []slicelen(s) // 0cap(s) // 0for range s // iterates zero timess[i] // panic: index out of range
A nil
slice, in addition to not indexed, other operations are possible, when you need to fill the value of the function can be used append
, slice will automatically expand. So nil
what is the underlying structure of the slice? According to the official documentation, Slice has three elements, namely length, capacity, pointers to arrays:
Slice
When there are elements:
Slice
So we do not need to worry about the size of slice, use append words slice will automatically expand. (the video said slice automatic expansion speed quickly, do not worry about performance issues, this is debatable, in determining the size of slice only once memory allocation is always good)
Map
For go, Map,function,channel are special pointers, pointing to their specific implementations, which we can use for the time being.
// nil mapsvar m map[t]ulen(m) // 0for range m // iterates zero timesv, ok := m[i] // zero(u), falsem[i] = x // panic: assignment to entry in nil map
For nil
The map, we can simply think of it as a read-only map, unable to write operations, otherwise it will be panic. So nil
what's the use of a map? Take a look at this example:
func NewGet(url string, headers map[string]string) (*http.Request, error) { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } for k, v := range headers { req.Header.Set(k, v) } return req, nil}
For NewGet
example, we need to pass in a parameter of type map, and this function just reads this parameter, we can pass in a non-null value:
NewGet("http://google.com", map[string]string{ "USER_AGENT": "golang/gopher",},)
or so:
NewGet("http://google.com", map[string]string{})
But as I said earlier, the value of map 0 is nil
, so when it's header
empty, we can also pass it directly to a nil
:
NewGet("http://google.com", nil)
Isn't it a lot simpler? So, nil
read the map as a read-only, empty map.
Channel
// nil channelsvar c chan t<- c // blocks foreverc <- x // blocks foreverclose(c) // panic: close of nil channel
Closing a nil
channel will cause the program panic
(how to close the channel can see this article: How to gracefully close the go channel) For example, if there are now two channel responsible for input, a channel is responsible for the summary, Simple implementation Code:
func merge(out chan<- int, a, b <-chan int) { for { select { case v := <-a: out <- v case v := <- b: out <- v } }}
If A or B is turned off in an external call, then the 0 is read continuously from a or B, which is not the same as what we want, we want to close A and B and then stop summarizing, modify the code:
func merge(out chan<- int, a, b <-chan int) { for a != nil || b != nil { select { case v, ok := <-a: if !ok { a = nil fmt.Println("a is nil") continue } out <- v case v, ok := <-b: if !ok { b = nil fmt.Println("b is nil") continue } out <- v } } fmt.Println("close out") close(out)}
When you know that the channel is closed, set the channel value to nil, which is equivalent to deactivating the Select Case clause because nil
the channel is always blocked.
Interface
Interface is not a pointer, its underlying implementation consists of two parts, a type, a value, which is similar to: (Type, value). Only if the type and value are nil
equal nil
. Take a look at the following code:
func do() error { // error(*doError, nil) var err *doError return err // nil of type *doError}func main() { err := do() fmt.Println(err == nil)}
The output is the result false
. The do
function declares a *doErro
variable and err
then returns that the return value is an error
interface, but this time the type has become: (*doerror,nil), so and nil
certainly not equal. So when we write a function, don't declare the specific error variable, but should return directly nil
:
func do() error { return nil}
Take another look at this example:
func do() *doError { // nil of type *doError return nil}func wrapDo() error { // error (*doError, nil) return do() // nil of type *doError}func main() { err := wrapDo() // error (*doError, nil) fmt.Println(err == nil) // false}
And here's the final output false
. Why is it? Although the function returns a type, it returns a type, that is, it wrapDo
error
do
*doError
becomes (*doerror,nil), and the nature is nil
not equal. Therefore, do not return the specific error type. Follow these two recommendations before you can use them with confidence if x != nil
.
Summarize
After watching the video, I found that nil
there are so many useful, it is really unexpected joy.
There is a lot of dry-filled video on the tubing, you can learn more.