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:
- start with the prefix
+build
followed by one or more spaces - located at top of file before the package declaration
- have at least one blank line between it and package declaration to prevent it from being treated as package documentation
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:
- Build tags starting with ! are negated
- Build tags separated by spaces are logically OR’d
- Build tags separated by commas are logically AND’d
- Build constraints on multiple lines are logically AND’d
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: