Tellor Docs
Search…
Coding style guide
Coding style guide for contributors.
This document details the official style guides for the various languages we use in the project. Feel free to familiarize yourself with and refer to this document during code reviews. If something in our codebase does not match the style, it means it was missed or it was written before this document. Help wanted to fix it! (:
Generally, we care about:
  • Readability, so low Cognitive Load.
  • Maintainability. We avoid the code that surprises.
  • Performance only for critical path and without compromising readability.
  • Testability. Even if it means some changes to the production code, like timeNow func() time.Time mock.
  • Consistency: If some pattern repeats, it means fewer surprises.
Some style is enforced by our linters and is covered in separate smaller sections. Please look there if you want to embrace some of the rules in your own project! Some of those are currently impossible to detect with linters. Ideally, everything would be automated. (:

Go

For code written in Go we use the standard Go style guides (Effective Go, CodeReviewComments) with a few additional rules that make certain areas stricter than the standard guides. This ensures even better consistency in modern distributed systems, where reliability, performance, and maintainability are extremely important.

Development / Code Review

In this section, we will go through rules that on top of the standard guides that we apply during development and code reviews.
NOTE: If you know that any of those rules can be enabled by some linter, automatically, let us know! (:

Reliability

The coding style is not purely about what is ugly and what is not. It's mainly to make sure programs are reliable for running on production 24h per day without causing incidents. The following rules are describing some unhealthy patterns we have seen across the Go community that are often forgotten. Those things can be considered bugs or can significantly increase the chances of introducing a bug.

Defers: Don't Forget to Check Returned Errors

It's easy to forget to check the error returned by a Close method that we deferred.
1
f, err := os.Open(...)
2
if err != nil {
3
// handle..
4
}
5
defer f.Close() // What if an error occurs here?
6
​
7
// Write something to file... etc.
Copied!
Unchecked errors like this can lead to major bugs. Consider the above example: the *os.File Close method can be responsible for actually flushing to the file, so if an error occurs at that point, the whole write might be aborted! 😱
Always check errors! To make it consistent and not distracting, use runutil helper package, e.g.:
1
// Use `CloseWithErrCapture` if you want to close and fail the function or
2
// method on a `f.Close` error (make sure the `error` return argument is
3
// named as `err`). If the error is already present, `CloseWithErrCapture`
4
// will append (not wrap) the `f.Close` error if any.
5
defer runutil.CloseWithErrCapture(&err, f, "close file")
6
​
7
// Use `CloseWithLogOnErr` if you want to close and log error on `Warn`
8
// level on a `f.Close` error.
9
defer runutil.CloseWithLogOnErr(logger, f, "close file")
Copied!
Avoid πŸ”₯
1
func writeToFile(...) error {
2
f, err := os.Open(...)
3
if err != nil {
4
return err
5
}
6
defer f.Close() // What if an error occurs here?
7
​
8
// Write something to file...
9
return nil
10
}
Copied!
Better πŸ€“
1
func writeToFile(...) (err error) {
2
f, err := os.Open(...)
3
if err != nil {
4
return err
5
}
6
// Now all is handled well.
7
defer runutil.CloseWithErrCapture(&err, f, "close file")
8
​
9
// Write something to file...
10
return nil
11
}
Copied!

Exhaust Readers

One of the most common bugs is forgetting to close or fully read the bodies of HTTP requests and responses, especially on error. If you read the body of such structures, you can use the runutil helper as well:
1
defer runutil.ExhaustCloseWithLogOnErr(logger, resp.Body, "close response")
Copied!
Avoid πŸ”₯
1
resp, err := http.Get("http://example.com/")
2
if err != nil {
3
// handle...
4
}
5
defer runutil.CloseWithLogOnErr(logger, resp.Body, "close response")
6
​
7
scanner := bufio.NewScanner(resp.Body)
8
// If any error happens and we return in the middle of scanning
9
// body, we can end up with unread buffer, which
10
// will use memory and hold TCP connection!
11
for scanner.Scan() {
Copied!
Better πŸ€“
1
resp, err := http.Get("http://example.com/")
2
if err != nil {
3
// handle...
4
}
5
defer runutil.ExhaustCloseWithLogOnErr(logger, resp.Body, "close response")
6
​
7
scanner := bufio.NewScanner(resp.Body)
8
// If any error happens and we return in the middle of scanning body,
9
// defer will handle all well.
10
for scanner.Scan() {
Copied!

Avoid Globals

No globals other than const are allowed. Period. This means also, no init functions.

Never Use Panics

Never use them. If some dependency uses it, use recover. Also, consider avoiding that dependency. πŸ™ˆ

Avoid Using the reflect or unsafe Packages

Use those only for very specific, critical cases. Especially reflect tend to be very slow. For testing code, it's fine to use reflect.

Avoid variable shadowing

Variable shadowing is when you use the same variable name in a smaller scope that "shadows". This is very dangerous as it leads to many surprises. It's extremely hard to debug such problems as they might appear in unrelated parts of the code. And what's broken is tiny : or lack of it.
Avoid πŸ”₯
1
var client ClientInterface
2
if clientTypeASpecified {
3
// Ups - typo, should be =`
4
client, err := clienttypea.NewClient(...)
5
if err != nil {
6
// handle err
7
}
8
level.Info(logger).Log("msg", "created client", "type", client.Type)
9
} else {
10
// Ups - typo, should be =`
11
client, err := clienttypea.NewClient(...)
12
level.Info(logger).Log("msg", "noop client will be used", "type", client.Type)
13
}
14
​
15
// In some further deeper part of the code...
16
resp, err := client.Call(....) // nil pointer panic!
Copied!
Better πŸ€“
1
var client ClientInterface = NewNoop(...)
2
if clientTypeASpecified {
3
c, err := clienttypea.NewClient(...)
4
if err != nil {
5
// handle err
6
}
7
client = c
8
}
9
level.Info(logger).Log("msg", "created client", "type", c.Type)
10
​
11
resp, err := client.Call(....)
Copied!
This is also why we recommend to scope errors if you can:
1
if err := doSomething(); err != nil {
2
// handle err
3
}
Copied!
While it's not yet configured, we might think consider not permitting variable shadowing with golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow in future. There was even Go 2 proposal for disabling this in the language itself, but was rejected:
Similar to this problem is the package name shadowing. While it is less dangerous, it can cause similar issues, so avoid package shadowing if you can.

Performance

After all better performance is important and this might require some additional patterns in our code. With those patterns, we try to not sacrifice the readability and apply those only on the critical code paths.
Keep in mind to always measure the results. The Go performance relies on many hidden things and tweaks, so the good micro benchmark, following with the real system load test is in most times required to tell if optimization makes sense.

Pre-allocating Slices and Maps

Try to always preallocate slices and map. If you know the number of elements you want to put apriori, use that knowledge! This significantly improves the latency of such code. Consider this as micro optimization, however, it's a good pattern to do it always, as it does not add much complexity. Performance wise, it's only relevant for critical, code paths with big arrays.
NOTE: This is because, in very simple view, the Go runtime allocates 2 times the current size. So if you expect million of elements, Go will do many allocations on append in between instead of just one if you preallocate.
Avoid πŸ”₯
1
func copyIntoSliceAndMap(biggy []string) (a []string, b map[string]struct{})
2
b = map[string]struct{}{}
3
​
4
for _, item := range biggy {
5
a = append(a, item)
6
b[item] = struct{}
7
}
8
}
Copied!
Better πŸ€“
1
func copyIntoSliceAndMap(biggy []string) (a []string, b map[string]struct{})
2
b = make(map[string]struct{}, len(biggy))
3
a = make([]string, len(biggy))
4
​
5
// Copy will not even work without pre-allocation.
6
copy(a, biggy)
7
for _, item := range biggy {
8
b[item] = struct{}
9
}
10
}
Copied!

Reuse arrays

To extend the above point, there are cases where you don't need to allocate new space in memory all the time. If you repeat the certain operation on slices sequentially and you just release the array on every iteration, it's reasonable to reuse the underlying array for those. This can give quite enormous gains for critical paths. Unfortunately, currently there is no way to reuse the underlying array for maps.
NOTE: Why you cannot just allocate slice and release and in new iteration allocate and release again etc? Go should know it has available space and just reuses that no? (: Well, it's not that easy. TL;DR is that Go Garbage Collection runs periodically or on certain cases (big heap), but definitely not on every iteration of your loop (that would be super slow). Read more in details here.
Avoid πŸ”₯
1
var messages []string
2
for _, msg := range recv {
3
messages = append(messages, msg)
4
​
5
if len(messages) > maxMessageLen {
6
marshalAndSend(messages)
7
// This creates new array. Previous array
8
// will be garbage collected only after
9
// some time (seconds), which
10
// can create enormous memory pressure.
11
messages = []string
12
}
13
}
Copied!
Better πŸ€“
1
var messages []string
2
for _, msg := range recv {
3
messages = append(messages, msg)
4
​
5
if len(messages) > maxMessageLen {
6
marshalAndSend(messages)
7
// Instead of new array, reuse
8
// the same, with the same capacity,
9
// just length equals to zero.
10
messages = messages[:0]
11
}
12
}
Copied!

Readability

The part that all Gophers love ❀️ How to make code more readable?
For the Team, readability is about programming in a way that does not surprise the reader of the code. All the details and inconsistencies can distract or mislead the reader, so every character or newline might matter. That's why we might be spending more time on every Pull Requests' review, especially in the beginning, but for a good reason! To make sure we can quickly understand, extend and fix problems with our system.

Keep the Interface Narrow; Avoid Shallow Functions

This is connected more to the API design than coding, but even during small coding decisions it matter. For example how you define functions or methods. There are two general rules:
  • Simpler (usually it means smaller) interfaces are better. This might mean a smaller, simpler function signature as well as fewer methods
    in the interfaces. Try to group interfaces based on functionality to expose at max 1-3 methods if possible.
Avoid πŸ”₯
1
// Compactor aka: The Big Boy. Such big interface is really useless ):
2
type Compactor interface {
3
Compact(ctx context.Context) error
4
FetchMeta(ctx context.Context) (metas map[ulid.ULID]*metadata.Meta, partial map[ulid.ULID]error, err error)
5
UpdateOnMetaChange(func([]metadata.Meta, error))
6
SyncMetas(ctx context.Context) error
7
Groups() (res []*Group, err error)
8
GarbageCollect(ctx context.Context) error
9
ApplyRetentionPolicyByResolution(ctx context.Context, logger log.Logger, bkt objstore.Bucket) error
10
BestEffortCleanAbortedPartialUploads(ctx context.Context, bkt objstore.Bucket)
11
DeleteMarkedBlocks(ctx context.Context) error
12
Downsample(ctx context.Context, logger log.Logger, metrics *DownsampleMetrics, bkt objstore.Bucket) error
13
}
Copied!
Better πŸ€“
1
// Smaller interfaces with a smaller number of arguments allow functional grouping, clean composition and clear testability.
2
type Compactor interface {
3
Compact(ctx context.Context) error
4
​
5
}
6
​
7
type Downsampler interface {
8
Downsample(ctx context.Context) error
9
}
10
​
11
type MetaFetcher interface {
12
Fetch(ctx context.Context) (metas map[ulid.ULID]*metadata.Meta, partial map[ulid.ULID]error, err error)
13
UpdateOnChange(func([]metadata.Meta, error))
14
}
15
​
16
type Syncer interface {
17
SyncMetas(ctx context.Context) error
18
Groups() (res []*Group, err error)
19
GarbageCollect(ctx context.Context) error
20
}
21
​
22
type RetentionKeeper interface {
23
Apply(ctx context.Context) error
24
}
25
​
26
type Cleaner interface {
27
DeleteMarkedBlocks(ctx context.Context) error
28
BestEffortCleanAbortedPartialUploads(ctx context.Context)
29
}
Copied!
  • It's better if you can hide more unnecessary complexity from the user. This means that having shallow function introduce
    more cognitive load to understand the function name or navigate to implementation to understand it better. It might be much
    more readable to inline those few lines directly on the caller side.
Avoid πŸ”₯
1
// Some code...
2
s.doSomethingAndHandleError()
3
​
4
// Some code...
5
}
6
​
7
func (s *myStruct) doSomethingAndHandleError() {
8
if err := doSomething(); err != nil {
9
level.Error(s.logger).Log("msg" "failed to do something; sorry", "err", err)
10
}
11
}
Copied!
Better πŸ€“
1
// Some code...
2
if err := doSomething(); err != nil {
3
level.Error(s.logger).Log("msg" "failed to do something; sorry", "err", err)
4
}
5
​
6
// Some code...
7
}
Copied!
This is a little bit connected to There should be one-- and preferably only one --obvious way to do it and DRY rules. If you have more ways of doing something than one, it means you have a wider interface, allowing more opportunities for errors, ambiguity and maintenance burden.
Avoid πŸ”₯
1
// We have here SIX potential ways the caller can get an ID. Can you find all of them?
2
​
3
type Block struct {
4
// Things...
5
ID ulid.ULID
6
​
7
mtx sync.Mutex
8
}
9
​
10
func (b *Block) Lock() { b.mtx.Lock() }
11
​
12
func (b *Block) Unlock() { b.mtx.Unlock() }
13
​
14
func (b *Block) ID() ulid.ULID {
15
b.mtx.Lock()
16
defer b.mtx.Unlock()
17
return b.ID
18
}
19
​
20
func (b *Block) IDNoLock() ulid.ULID { return b.ID }
Copied!
Better πŸ€“
1
type Block struct {
2
// Things...
3
​
4
id ulid.ULID
5
mtx sync.Mutex
6
}
7
​
8
func (b *Block) ID() ulid.ULID {
9
b.mtx.Lock()
10
defer b.mtx.Unlock()
11
return b.id
12
}
Copied!

Use Named Return Parameters Carefully

It's OK to name return parameters if the types do not give enough information about what function or method actually returns. Another use case is when you want to define a variable, e.g. a slice.
IMPORTANT: never use naked return statements with named return parameters. This compiles but it makes returning values implicit and thus more prone to surprises.

Clean Defer Only if Function Fails

There is a way to sacrifice defer in order to properly close all on each error. Repetition makes it easier to make an error and forget something when changing the code, so on-error deferring is doable:
Avoid πŸ”₯
1
func OpenSomeFileAndDoSomeStuff() (*os.File, error) {
2
f, err := os.OpenFile("file.txt", os.O_RDONLY, 0)
3
if err != nil {
4
return nil, err
5
}
6
​
7
if err := doStuff1(); err != nil {
8
runutil.CloseWithErrCapture(&err, f, "close file")
9
return nil, err
10
}
11
if err := doStuff2(); err != nil {
12
runutil.CloseWithErrCapture(&err, f, "close file")
13
return nil, err
14
}
15
if err := doStuff232241(); err != nil {
16
// Ups.. forgot to close file here.
17
return nil, err
18
}
19
return f, nil
20
}
Copied!
Better πŸ€“
1
func OpenSomeFileAndDoSomeStuff() (f *os.File, err error) {
2
f, err = os.OpenFile("file.txt", os.O_RDONLY, 0)
3
if err != nil {
4
return nil, err
5
}
6
defer func() {
7
if err != nil {
8
runutil.CloseWithErrCapture(&err, f, "close file")
9
}
10
}
11
​
12
if err := doStuff1(); err != nil {
13
return nil, err
14
}
15
if err := doStuff2(); err != nil {
16
return nil, err
17
}
18
if err := doStuff232241(); err != nil {
19
return nil, err
20
}
21
return f, nil
22
}
Copied!

Explicitly Handle Returned Errors

Always handle returned errors. It does not mean you cannot "ignore" the error for some reason, e.g. if we know implementation will not return anything meaningful. You can ignore the error, but do so explicitly:
Avoid πŸ”₯
1
someMethodThatReturnsError(...)
Copied!
Better πŸ€“
1
_ = someMethodThatReturnsError(...)
Copied!
The exception: well-known cases such as level.Debug|Warn etc and fmt.Fprint*

Avoid Defining Variables Used Only Once.

It's tempting to define a variable as an intermittent step to create something bigger. Avoid defining such a variable if it's used only once. When you create a variable the reader expects some other usage of this variable than one, so it can be annoying to every time double check that and realize that it's only used once.
Avoid πŸ”₯
1
someConfig := a.GetConfig()
2
address124 := someConfig.Addresses[124]
3
addressStr := fmt.Sprintf("%s:%d", address124.Host, address124.Port)
4
​
5
c := &MyType{HostPort: addressStr, SomeOther: thing}
6
return c
Copied!
Better πŸ€“
1
// This variable is required for potentially consistent results. It is used twice.
2
someConfig := a.FetchConfig()
3
return &MyType{
4
HostPort: fmt.Sprintf("%s:%d", someConfig.Addresses[124].Host, someConfig.Addresses[124].Port),
5
SomeOther: thing,
6
}
Copied!

Only Two Ways of Formatting Functions/Methods

Prefer function/method definitions with arguments in a single line. If it's too wide, put each argument on a new line.
Avoid πŸ”₯
1
func function(argument1 int, argument2 string,
2
argument3 time.Duration, argument4 someType,
3
argument5 float64, argument6 time.Time,
4
) (ret int, err error) {
Copied!
Better πŸ€“
1
func function(
2
argument1 int,
3
argument2 string,
4
argument3 time.Duration,
5
argument4 someType,
6
argument5 float64,
7
argument6 time.Time,
8
) (ret int, err error)
Copied!
This applies for both calling and defining method / function.
NOTE: One exception would be when you expect the variadic (e.g. ...string) arguments to be filled in pairs, e.g:
1
level.Info(logger).Log(
2
"msg", "found something epic during compaction; this looks amazing",
3
"compNumber", compNumber,
4
"block", id,
5
"elapsed", timeElapsed,
6
)
Copied!

Control Structure: Prefer early returns and avoid else

In most of the cases, you don't need else. You can usually use continue, break or return to end an if block. This enables having one less indent and better consistency so code is more readable.
Avoid πŸ”₯
1
for _, elem := range elems {
2
if a == 1 {
3
something[i] = "yes"
4
} else
5
something[i] = "no"
6
}
7
}
Copied!
Better πŸ€“
1
for _, elem := range elems {
2
if a == 1 {
3
something[i] = "yes"
4
continue
5
}
6
something[i] = "no"
7
}
Copied!

Wrap Errors for More Context; Don't Repeat "failed ..." There.

We use pkg/errors package for errors. We prefer it over standard wrapping with fmt.Errorf + %w, as errors.Wrap is explicit. It's easy to by accident replace %w with %v or to add extra inconsistent characters to the string.
Use pkg/errors.Wrap to wrap errors for future context when errors occur. It's recommended to add more interesting variables to add context using errors.Wrapf, e.g. file names, IDs or things that fail, etc.
When using Errorf,Warpf put arguments at the end to make it easyer to read and use %v to reduce human error if the argument type changed.
Difficult to read πŸ”₯
1
errors.Errorf("insufficient balance %s, mining stake requires %s", a, b)
Copied!
Better πŸ€“
1
errors.Errorf("insufficient mining stake TRB balance - actual:%v, required:%v", a, b)
Copied!
NOTE: never prefix wrap messages with wording like failed..., couldn't... or error occurred while.... Just describe what we wanted to do when the failure occurred. Those prefixes are just noise. We are wrapping error, so it's obvious that some error occurred, right? (: Improve readability and consider avoiding those.
Avoid πŸ”₯
1
fmt.Errorf("error while reading from file %s: %w", f.Name, err)
Copied!
Better πŸ€“
1
errors.Wrapf(err, "read file %s", f.Name)
Copied!

Use the Blank Identifier _

Blank identifiers are very useful to mark variables that are not used. Consider the following cases:
1
// We don't need the second return parameter.
2
// Let's use the blank identifier instead.
3
a, _, err := function1(...)
4
if err != nil {
5
// handle err
6
}
Copied!
1
// We don't need to use this variable, we
2
// just want to make sure TypeA implements InterfaceA.
3
var _ InterfaceA = TypeA
Copied!
1
// We don't use context argument; let's use the blank
2
// identifier to make it clear.
3
func (t *Type) SomeMethod(_ context.Context, abc int) error {
Copied!

Rules for Log Messages

The project uses go-kit logger. This means that we expect log lines to have a certain structure. Structure means that instead of adding variables to the message, those should be passed as separate fields. Keep in mind that all log lines should be lowercase (readability and consistency) and all struct keys are using camelCase. It's suggested to keep key names short and consistent. For example, if we always use block for block ID, let's not use in the other single log message id.
Avoid πŸ”₯
1
level.Info(logger).Log("msg", fmt.Sprintf("Found something epic during compaction number %v. This looks amazing.", compactionNumber),
2
"block_id", id, "elapsed-time", timeElapsed)
Copied!
Better πŸ€“
1
level.Info(logger).Log("msg", "found something epic during compaction; this looks amazing", "compNumber", compNumber,
2
"block", id, "elapsed", timeElapsed)
Copied!
Additionally, there are certain rules we suggest while using different log levels:
  • level.Info: Should always have msg field. It should be used only for important events that we expect to happen not too
    often.
  • level.Debug: Should always have msg field. It can be a bit more spammy, but should not be everywhere as well. Use it
    only when you want to really dive into some problems in certain areas.
  • level.Warn: Should have either msg or err or both fields. They should warn about events that are suspicious and to investigate
    but the process can gracefully mitigate it. Always try to describe how it was mitigated, what action will be performed e.g. value will be skipped
  • level.Error: Should have either msg or err or both fields. Use it only for a critical event.

Comment Necessary Surprises

Comments are not the best. They age quickly and the compiler does not fail if you will forget to update them. So use comments only when necessary. And it is necessary to comment on code that can surprise the user. Sometimes, complexity is necessary, for example for performance. Comment in this case why such optimization was needed. If something was done temporarily add TODO(<github name>): <something, with GitHub issue link ideally>.

Testing

Table Tests

Use table-driven tests that use t.Run for readability. They are easy to read and allows to add a clean description of each test case. Adding or adapting test cases is also easier.
Avoid πŸ”₯
1
host, port, err := net.SplitHostPort("1.2.3.4:1234")
2
testutil.Ok(t, err)
3
testutil.Equals(t, "1.2.3.4", host)
4
testutil.Equals(t, "1234", port)
5
​
6
host, port, err = net.SplitHostPort("1.2.3.4:something")
7
testutil.Ok(t, err)
8
testutil.Equals(t, "1.2.3.4", host)
9
testutil.Equals(t, "http", port)
10
​
11
host, port, err = net.SplitHostPort(":1234")
12
testutil.Ok(t, err)
13
testutil.Equals(t, "", host)
14
testutil.Equals(t, "1234", port)
15
​
16
host, port, err = net.SplitHostPort("yolo")
17
testutil.NotOk(t, err)
Copied!
Better πŸ€“
1
for _, tcase := range []struct{
2
name string
3
​
4
input string
5
​
6
expectedHost string
7
expectedPort string
8
expectedErr error
9
}{
10
{
11
name: "host and port",
12
​
13
input: "1.2.3.4:1234",
14
expectedHost: "1.2.3.4",
15
expectedPort: "1234",
16
},
17
{
18
name: "host and named port",
19
​
20
input: "1.2.3.4:something",
21
expectedHost: "1.2.3.4",
22
expectedPort: "something",
23
},
24
{
25
name: "just port",
26
​
27
input: ":1234",
28
expectedHost: "",
29
expectedPort: "1234",
30
},
31
{
32
name: "not valid hostport",
33
​
34
input: "yolo",
35
expectedErr: errors.New("<exact error>")
36
},
37
}{
38
t.Run(tcase.name, func(t *testing.T) {
39
host, port, err := net.SplitHostPort(tcase.input)
40
if tcase.expectedErr != nil {
41
testutil.NotOk(t, err)
42
testutil.Equals(t, tcase.expectedErr, err)
43
return
44
}
45
testutil.Ok(t, err)
46
testutil.Equals(t, tcase.expectedHost, host)
47
testutil.Equals(t, tcase.expectedPort, port)
48
})
49
}
Copied!

Tests for Packages / Structs That Involve time package.

Avoid unit testing based on real-time. Always try to mock time that is used within struct by using, for example, a timeNow func() time.Time field. For production code, you can initialize the field with time.Now. For test code, you can set a custom time that will be used by the struct.
Avoid πŸ”₯
1
func (s *SomeType) IsExpired(created time.Time) bool {
2
// Code is hardly testable.
3
return time.Since(created) >= s.expiryDuration
4
}
Copied!
Better πŸ€“
1
func (s *SomeType) IsExpired(created time.Time) bool {
2
// s.timeNow is time.Now on production, mocked in tests.
3
return created.Add(s.expiryDuration).Before(s.timeNow())
4
}
Copied!

Enforced by Linters

This is the list of rules we ensure automatically. This section is for those who are curious why such linting rules were added or want similar ones in their Go project. πŸ€—

Avoid Prints

Never use print. Always use a passed go-kit/log.Logger.

Comments Should be Full Sentences

All comments should be full sentences. They should start with an uppercase letter and end with a period.

Bash

Overall try to NOT use bash. For scripts longer than 30 lines, consider writing it in Go as we did here.
If you have to, we follow the Google Shell style guide: https://google.github.io/styleguide/shellguide.html​
Last modified 11mo ago