Go Style Guide
Unless specified otherwise below, stick to golang’s CodeReviewComments.
Generally the code should be formatted with gofmt
(checked by CI).
Lines must be at most 100 characters long (checked by CI via lll
).
Naming
We use mixedCaps notation as recommended by Effective Go. Perhaps unintuitively, Go
treats ID
as an initialism, and we treat If
as a word. The following
rules apply (note that a significant part of the code base uses other
notations; these should be refactored, however):
Use
sd
orSD
to refer to the SCION Daemon, notSciond
orSCIOND
.Use
IfID
orifID
for SCION Interface Identifiers, notIFID
norInterfaceID
norintfID
.Use
IfIDSomething
orifIDSomething
when concatenatingifID
withsomething
.Use
Svc
orsvc
for SCION Service Addresses, notSVC
orService
.Use
TRC
ortrc
for Trust Root Configurations, notTrc
.
Imports (checked by CI)
Imports are grouped (separated by empty line) in the following order:
standard lib
third-party packages
our packages
Within each group the imports are alphabetically sorted.
Function declaration over multiple lines
If a function declaration uses more than 1 line, each parameter should be declared on a separate line and the first line of the function body should be empty:
func usingMultipleLines(
foo int,
bar []string,
qux bool,
) error {
// start the code here
}
Abbreviations
For variable names common abbreviations should be preferred to full names, if they are clear from the context, or used across the codebase.
Examples:
Seg
instead ofSegment
Msgr
instead ofMessenger
Sync
instead ofSynchronization
Specialities
goroutines should always call defer log.HandlePanic()
as the first statement (checked by CI).
Logging
To use logging, import
"github.com/scionproto/scion/go/lib/log"
.The logging package supports three logging levels:
Debug: entries that are aimed only at developers, and include very low-level details. These should never be enabled on a production machine. Examples of such entries may include opening a socket, receiving a network message, or loading a file from the disk.
Info: entries that either contain high-level information about what the application is doing, or “errors” that are part of normal operation and have been handled by the code. Examples of such entries may include: issuing a new certificate for a client, having the authentication of an RPC call fail, or timing out when trying to connect to a server.
Error: entries about severe problems encountered by the application. The application might even need to terminate due to such an error. Example of such entries may include: the database is unreachable, the database schema is corrupted, or writing a file has failed due to insufficient disk space.
Do not use
log.Root().New(...)
, instead use New directly:log.New(...)
.Keys should be snake case; use
log.Debug("msg", "some_key", "foo")
instead oflog.Debug("msg", "someKey", "foo")
or other variants.Try to not repeat key-value pairs in logging calls that are close-by; derive a new logging context instead (e.g., if multiple logging calls refer to a
"request"
for"Foo"
, create a sublogger with this context by callingnewLogger = parentLogger.New("request", "Foo")
and then usenewLogger.Debug("x")
).An empty
log.New()
has no impact and should be omitted.
Here is an example of how logging could be added to a type:
type Server struct {
// Logger is used by the server to log information about internal events. If nil, logging
// is disabled.
Logger Logger
}
// Use a func because this is a godoc example, but this would normally be something like
// s.Run().
Run := func(s *Server) {
if s.Logger == nil {
s.Logger = DiscardLogger{}
}
s.Logger.Debug("this message is discarded now")
}
Run(&Server{})
Metrics
Metrics definition and interactions should be consistent throughout the code base. A common pattern makes it easier for developers to implement and refactor metrics, and for operators to understand where metrics are coming from. As a bonus, we should leverage the type system to help us spot as many errors as possible.
To write code that both includes metrics, and is testable, we use the metric
interfaces defined in the pkg/metrics/v2
package.
A simple example with labels (note that Giant
’s metrics can be unit tested by
mocking the counter):
type Giant struct {
MagicBeansEaten metrics.Counter
RedPillsEaten metrics.Counter
BluePillsEaten metrics.Counter
}
counter := prometheus.NewCounter(prometheus.CounterOpts{
Name: "magic_beans_eaten_total",
Help: "Number of magic beans eaten.",
})
pillCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "pills_eaten_total",
Help: "Number of pills eaten.",
}, []string{"color"})
giant := Giant{
MagicBeansEaten: counter,
RedPillsEaten: pillCounter.WithLabelValues("red"),
BluePillsEaten: pillCounter.With(prometheus.Labels{
"color": "blue",
}),
}
giant.MagicBeansEaten.Add(4)
giant.RedPillsEaten.Add(2)
giant.BluePillsEaten.Add(1)
Calling code can later create Giant
objects with Prometheus metric reporting
by plugging a prometheus counter as the Counter
as shown in the example.
Note
Some packages have metrics
packages that define labels and initialize
metrics (see the go/cs/beacon/metrics
package for an example). While this
is also ok, the recommended way is to define labels in the package itself and
initialize metrics in main
.
Best Practices
Namespace should be one word.
Subsystem should be one word (if present).
Use values that can be searched with regex. E.g. prepend
err_
for every error result.snake_case
label names and values.Put shared label names and values into
go/lib/prom
.Always initialize
CounterVec
to avoid hidden metrics link.