The Magic of Go Comments

Comments are a valuable tool for documenting and communicating information about code. They are a common feature in nearly every programming language and Go is no exception. However, comments in Go programs can do far more than providing information readers of the code. In this article I will highlight some lesser known uses of comments within Go that have special – almost magical – behavior.

Go Comment Syntax

Taking from its syntactic heritage in C, Go supports the familiar single line and multiple line comments using // and /* ... */ as follows:

// Everything to the end of the line is a comment
// Additional lines require another `//` marker

var foo int;    // Comments can start anywhere on a line

/* Everything until closing marker is comment
   This includes new lines

   And blank lines */

/* Multiline comments can be single lined too */

People with even a modest amount of programming experience in Go will recognize this comment syntax from code they have read or even code they have written themselves. However, while the content of comments is ignored by the Go compiler, it does not mean it is completely ignored by the go toolset. The reminder of this article will show a collection of specially formatted comments and how they are used within Go.

godoc Documentation Text

Probably the most familiar form of “magical” comments in Go are comments for Go’s built in documentation tool, godoc. godoc works by scanning all the .go files in a package (ignoring any _test.go files) for comments immediately preceding a declaration (without any intervening code or blank line(s)). godoc will then use the text of the comments to form the package’s documentation.

For example, to document a function we simply place one or more lines of comments on the line immediately before its declaration:

// Foo will foo the given string. If the string cannot be foo'd
// an error is returned.
func Foo(s string) error {
    ...
}

The same comment style can be used before any exported and package-level type, function, method, variable or constant declaration:

package objects

// Object is a generic something.
type Object struct {}

// Bar will bar Object, returning error if un-barable.
func (o Object) Bar() error {
    return nil
}

// List contains all currently registered Objects.
var List []Object

// MaxCount determines maximum number of objects allowed
const MaxCount = 50

Because only exported, package-level comments are handled, developers are free to use comments within method/function bodies without worrying about comments being inadvertently added to public documentation.

godoc also provides a means for generating package level documentation by parsing any comments found immediately before a package declaration:

// Package objects performs rudimentary accounting of objects.
package objects

It should be noted that godoc uses the first sentence of the package documentation comments when generating its index of packages (for instance see https://golang.org/pkg/) so be sure to write package descriptions accordingly.

As you can see, comments allow for a very easy method of providing developer and user documentation without complex syntax or additional documentation files.

Build Constraints

A second special use of comments within Go is build constraints.

One key feature of Go as a programming language is its support for a variety of operating systems and architectures. Often the same code can be used for multiple platforms, but in some cases there is OS or architecture specific code that should only be used for certain targets. The standard go build tool can handle some such situations by understanding that programs ending with an OS name and/or an architecture should only be used for targets matching those tags. For instance, a file named, foo_linux.go, will only be compiled for the Linux operating system, foo_amd64.go for AMD64 architectures and foo_windows_amd64.go for 64-bit Windows systems running on AMD64 architectures.

However, these naming conventions fail in more complex cases, for instance when the same code can be used for multiple (but not all) operating systems. For these cases, Go has the concept of build constraints – specially crafted comments that are read by go build to determine which files to reference when compiling a Go program.

Build constraints are comments that adhere to the following rules:

So rather than naming a file foo_linux.go we can just place the following comment at the start of the file foo.go:

// +build linux

package main
...

However, the power of build constraints arises when multiple architectures and/or operating systems are referenced. Go institutes the following rules for combining build constraints:

Given the above rules, the following constraint will limit the file to either Linux or Darwin (MacOS):

// +build linux darwin

package main
...

while this constraint requires both Windows and i386:

// +build windows,386

package main
...

The above constraint could also be written on two lines as follows:

// +build windows
// +build 386

package main
...

In addition to specifying the OS and architecture, build constraints can be used to ignore a file altogether through a common idiom of an ignore target (though any text that doesn’t match a valid architecture or operating system would work):

// +build ignore

package main
...

It should be noted these build constraints (and the aforementioned naming conventions) also work for test files, so it is possible to perform architecture/OS specific tests in a similar manner.

The full capabilities of built constraints is explained in detail in the go build documentation located here.

Generating Code

Another interesting alterative use for comments in Go is generating code via the go generate command. go generate is part of the standard Go toolkit and programmatically generates source (or other) files by running a user-specified external command. go generate works by scanning .go programs for specially crafted comments containing the command to be run and then executing them.

Specifically, go generate looks for a comment that starts with go:generate (with no whitespace between the comment marker and the text start), as follows:

//go:generate <command> <arguments>
...

Unlike build constraints, a go:generate comment can be located anywhere within a .go source file (though the typical Go idiom is to place them near the start of the file).

One common use of go generate is to provide human readable representations of numeric constants via the stringer tool available here. The stringer documentation provides the following example which explains its operation. Given a custom type, Pill, with enumerated constants:

type Pill int

const (
  Placebo Pill = iota
  Aspirin
  Ibuprofen
  Paracetamol
  Acetaminophen = Paracetamol
)

Running the command stringer -type Pill will create a new source file, pill_string.go that provides the following method:

func (p Pill) String() string

which allows one, for example, to print the name of the const like:

fmt.Printf("pill type: %s", pill)

But needing to remember the correct command and arguments for each applicable type in a package can be complex so instead we can add the following comment to a .go file in the package:

//go:generate stringer -type=Pill
...

and then running go generate will trigger the stringer call with the correct arguments to make our Pill string methods. Given this, one can see how stringer and go generate can be a great benefit to a programmer, especially in the case of multiple custom types in a package.

Cgo

The final special use of comments in Go that I will discuss is the C integration tool, Cgo. Cgo allows Go programs to call C code directly, allowing for reuse of established C libraries within Go. To use Cgo in a Go program, one first imports the pseudo-package “C”. Once imported, the Go program can then reference native C types like C.size_t and functions such as C.putchar().

However there are certain aspects of programming in C that do not translate easily to Go. To handle these, Cgo makes special use of comments that immediately precede the import "C" statement (known as the preamble in Cgo terms) as a means for providing various C-specific configuration items.

One such item is #include directives. Pretty much every C program requires #include directives to indicate location of header files. The Go language does not have any native equivalent command (import works on packages, not header files) so Cgo parses #include statements from the preamble. For example:

// #include <stdio.h>
// #include <errno.h>
import "C"

The preamble comments are not limited to only #include statements. In fact, any comments prior to the import statement will be treated as standard C code and can then be referenced by Go via the C package. For instance, with the preamble:

// #include <stdio.h>
//
// static void myprint(char *s) {
//     printf("%s\n", s)
// }
import "C"

we can then reference this newly defined C function within Go as follows:

C.myprint(C.String("foo"))

Finally, to handle compiler and similar options, Cgo introduces the #cgo directive which can be used to set environment variables, compiler flags and run pkg-config commands as follows:

// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo amd64 386 CFLAGS: -DX86=1
// #cgo LDFLAGS: -lpng
// #cgo pkg-config: png cairo
// #include <png.h>
import "C"

All of these preamble definitions help make programs using Cgo integrate (nearly) seamlessly with the go build tools rather than require the complexity of additional makefiles or other scripts.

Conclusion

I wrote this article in the hopes of introducing newer Go programmers to some lesser known uses of comments. I hope you found it interesting and useful. For more details on these concepts, please visit the following links: