effective-go chapter11

The blank identifier

We’ve mentioned the blank identifier a couple of times now, in the context offorrangeloopsandmaps. The blank identifier can be assigned or declared with any value of any type, with the value discarded harmlessly. It’s a bit like writing to the Unix/dev/nullfile: it represents a write-only value to be used as a place-holder where a variable is needed but the actual value is irrelevant. It has uses beyond those we’ve seen already.

The blank identifier in multiple assignment

The use of a blank identifier in aforrangeloop is a special case of a general situation: multiple assignment.

If an assignment requires multiple values on the left side, but one of the values will not be used by the program, a blank identifier on the left-hand-side of the assignment avoids the need to create a dummy variable and makes it clear that the value is to be discarded. For instance, when calling a function that returns a value and an error, but only the error is important, use the blank identifier to discard the irrelevant value.

1
2
3
if _, err := os.Stat(path); os.IsNotExist(err) {
truefmt.Printf("%s does not exist\n", path)
}

Occasionally you’ll see code that discards the error value in order to ignore the error; this is terrible practice. Always check error returns; they’re provided for a reason.

1
2
3
4
5
// Bad! This code will crash if path does not exist.
fi, _ := os.Stat(path)
if fi.IsDir() {
fmt.Printf("%s is a directory\n", path)
}

Unused imports and variables

It is an error to import a package or to declare a variable without using it. Unused imports bloat the program and slow compilation, while a variable that is initialized but not used is at least a wasted computation and perhaps indicative of a larger bug. When a program is under active development, however, unused imports and variables often arise and it can be annoying to delete them just to have the compilation proceed, only to have them be needed again later. The blank identifier provides a workaround.

This half-written program has two unused imports (fmtandio) and an unused variable (fd), so it will not compile, but it would be nice to see if the code so far is correct.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"io"
"log"
"os"
)

func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}

// TODO: use fd.

}

To silence complaints about the unused imports, use a blank identifier to refer to a symbol from the imported package. Similarly, assigning the unused variablefdto the blank identifier will silence the unused variable error. This version of the program does compile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"io"
"log"
"os"
)

var _ = fmt.Printf
// For debugging; delete when done.

var _ io.Reader
// For debugging; delete when done.


func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}

// TODO: use fd.

_ = fd
}

By convention, the global declarations to silence import errors should come right after the imports and be commented, both to make them easy to find and as a reminder to clean things up later.

Import for side effect

An unused import likefmtorioin the previous example should eventually be used or removed: blank assignments identify code as a work in progress. But sometimes it is useful to import a package only for its side effects, without any explicit use. For example, during itsinitfunction, thenet/http/pprofpackage registers HTTP handlers that provide debugging information. It has an exported API, but most clients need only the handler registration and access the data through a web page. To import the package only for its side effects, rename the package to the blank identifier:

1
import _ "net/http/pprof"

This form of import makes clear that the package is being imported for its side effects, because there is no other possible use of the package: in this file, it doesn’t have a name. (If it did, and we didn’t use that name, the compiler would reject the program.)

Interface checks

As we saw in the discussion ofinterfacesabove, a type need not declare explicitly that it implements an interface. Instead, a type implements the interface just by implementing the interface’s methods. In practice, most interface conversions are static and therefore checked at compile time. For example, passing an*os.Fileto a function expecting anio.Readerwill not compile unless*os.Fileimplements theio.Readerinterface.

Some interface checks do happen at run-time, though. One instance is in theencoding/jsonpackage, which defines aMarshalerinterface. When the JSON encoder receives a value that implements that interface, the encoder invokes the value’s marshaling method to convert it to JSON instead of doing the standard conversion. The encoder checks this property at run time with atype assertionlike:

1
m, ok := val.(json.Marshaler)

If it’s necessary only to ask whether a type implements an interface, without actually using the interface itself, perhaps as part of an error check, use the blank identifier to ignore the type-asserted value:

1
2
3
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

One place this situation arises is when it is necessary to guarantee within the package implementing the type that it actually satisfies the interface. If a type—for example,json.RawMessage—needs a custom JSON representation, it should implementjson.Marshaler, but there are no static conversions that would cause the compiler to verify this automatically. If the type inadvertently fails to satisfy the interface, the JSON encoder will still work, but will not use the custom implementation. To guarantee that the implementation is correct, a global declaration using the blank identifier can be used in the package:

1
var _ json.Marshaler = (*RawMessage)(nil)

In this declaration, the assignment involving a conversion of a*RawMessageto aMarshalerrequires that*RawMessageimplementsMarshaler, and that property will be checked at compile time. Should thejson.Marshalerinterface change, this package will no longer compile and we will be on notice that it needs to be updated.

The appearance of the blank identifier in this construct indicates that the declaration exists only for the type checking, not to create a variable. Don’t do this for every type that satisfies an interface, though. By convention, such declarations are only used when there are no static conversions already present in the code, which is a rare event.


20200131220947.png

0%