Golang Idioms and Best Practices: Part 1
Guidelines and conventions in Go (Golang) concerning naming conventions, code organization, steering clear of tools like init and panic, and incorporating documentation comments.
Tuesday, Dec 12, 2023
This is the first segment of a series of blog posts in which I plan to explore Golang idioms and best practices that I've encountered. This will include insights into practices that, looking back, I wish I had embraced earlier in my journey with Golang.
Quick Background
Prior to 2022, my development background predominantly revolved around Javascript, Typescript, NodeJS, and Salesforce Apex (During my Salesforce development days). However, upon acquiring proficiency in other programming languages like Go, my inclination has shifted away from using NodeJs and Typescript as the go-to choices for the backends I build.
Golang
Golang has quickly become one of my preferred programming languages to work with.
Go, as a statically typed and compiled language, is crafted for optimal performance and is often positioned as a contemporary alternative to the C programming language. Additionally, Go boasts features like a garbage collector, robust support for concurrency, a comprehensive standard library, and a straightforward syntax that contributes to its remarkable readability and ease of adoption.
Notably, the language has influenced Ryan Dahl, the creator of Node.js, prompting him to incorporate many Go-inspired ecosystem features into JavaScript through Deno.
Naming
Naming in respect to variables, functions, and interfaces are important to practice and adopt when writing Go. I personally find employing naming conventions early on in the learning process beneficial. I tend to spend too much effort and time on naming functions or variables than the actual problem at hand.
Variable Naming Conventions
- Use camelCase for variable names, capitalize for exported variables. (e.g
parseOptions
orParseOptions
) - Single letter names for indexes: i, j, k, ...
- For acronyms in names, use all capitals. (e.g.
MVC
,CRUD
,ACID
) - For collections, slices, arrays use repeated letters to represent, single letters in loop. (e.g. with a range loop
for t := range tt {
) - Avoid repeating package name in variable name.
Function Naming Conventions
Function names can naturally become too wordy and long.
You can combat this by omitting the following from function names:
- The type of a method's receiver. (e.g.
func (s *service) GetAccount() *account {...}
) - Whether an input or output is a pointer.
- Inputs and outputs types when there is no naming collision. (e.g
SetConfig
) - Avoid Get or Set naming. (e.g.
GetName
)
Examples
Repeating the name of the package
Method recievers, omit the name of the struct the reciever is for.
Avoid repeating the names of variables passed as parameters and types of return values
Interface Naming Convention
Name interfaces with a verb.
Here are some naming examples: [Stringer()
, Storer()
, Reader()
, Writer()
]
Examples from the io standard library package:
Real World Example
Imagine building a CLI package library and requiring an interface type as an argument. This value must satisfy the method signature of a function named Run
. This function must accept a slice of strings representing command-line arguments and return an integer.
Best practice, you'd name this interface ending with er
, giving the verb like intention.
Runn(er) interface
Grouping
Grouping is another idiomatic practice that contributes to more maintainable projects.
Variable Grouping
Group similar variable declarations of constants, variables, and type constants together.
Function Grouping
- Place exported functions closer to top of the file, while un-exported or private functions towards the bottom.
- Place similar functions together, especially those that serve similar purposes but vary by varying parameters and return types.
Mutext Grouping / Struct field Grouping
- When declaring mutex struct field, group next to the value the mutex is protecting race conditions with
- Name mutex field with the same name as the protected field with an appended, Mu.
This can be applied to other struct fields that have a tight-coupling behavior.
Avoiding init
What is init()?
Go's init()
is a package scope block that is invoked only once regardless of how many times the package is imported. It's primary purpose is to set up any required state before the package is used.
This can be useful for a lot of use cases however there are some drawbacks to it's use.
Use sync.Once
The init()
function makes it difficult to test and you, the developer, have no control over when this function is called.
Sometimes this special function is used to initialize a shared variable once to implement a singleton, making it concurrent safe.
This same pattern can be accomplished with the sync
standard library package's sync.Once
method.
Example
Imagine you have the following package, named store
.
This approach may function well initially, but when introducing concurrency to your application, you might encounter unexpected side effects. In such cases, resorting to the use of init()
becomes a viable solution.
While you are ensuring a single instance of the storage struct is allocated, you lose the ability to dictate when the init function is executed.
Utilize sync.Once to implement the Singleton pattern in your package, maintaining control over the moment when the sharedStorage variable is allocated in memory.
If you must panic...
Instead of invoking panic()
within the context of a function, opt to either return an error as the final return value or as the sole value when no computed result is returned, indicating an error.
If you must panic, prefix your function name with must
Or MustMarshalData
if the function is an exported one.
Go "Doc Comments"
If you come from a Javascript/Typescript background, you probably have come across JsDoc.
In Go, the "Doc Comments" idiom involves using lined-out comments to explain and document symbols such as packages, constants, functions, types, variable declarations, and commands. These comments are positioned directly above the respective subject they describe, enclosed by double backslash characters, //
.
When publishing internal or public packages, it's crucial to document your Go source code in adherence to the Doc Comment specifications. Particularly noteworthy is the importance of providing a doc comment for every exported (capitalized) named symbol.
An example of Doc Comments are as follows:
Doc Comments Support
The Go ecosystem incorporates a range of tools that facilitate the use of "Doc Comments". Within the standard library, Go offers support through packages such as go/doc and go/comment. These packages enable the extraction of documentation from Go source code. Additionally, external tools like cmd/pkgsite leverage this functionality to construct documentation websites, empowering sites like https://pkg.go.dev/.
Recap
We've touched on some of the idiomatic best practices when writing Go, in regards to:
- Naming Conventions
- Grouping Conventions
- Avoiding
init()
function when possible - Don't
panic()
, but if you must, prefix withmust
- Using "Doc Comments"