Golang Style Guide and Coding Standards

Author

Reads 1.3K

Workplace with modern laptop with program code on screen
Credit: pexels.com, Workplace with modern laptop with program code on screen

Golang has a strict style guide to ensure consistency and readability in code. This guide is based on the official Go style guide.

The use of blank lines is crucial in Golang, as it helps to separate logical sections of code. A blank line should be used between top-level functions, type declarations, and switch statements.

Consistent naming conventions are essential in Golang. The official guide recommends using camelCase for local variables and function names, and PascalCase for exported names. This helps to avoid confusion and makes code easier to read.

In Golang, it's a good practice to use the "go fmt" command to format code according to the style guide. This command can be run automatically during the build process to ensure consistency across the project.

If this caught your attention, see: Golang Test Command

Project Structure

Project Structure is crucial for a Go project's success. A well-structured project makes it easier for clients to understand what the package is for and how to use it.

Credit: youtube.com, The BEST Tool to Structure Golang Projects

Don't use the pkg pattern, as it goes against the Go philosophy of naming packages that signify what they contain. This pattern was used in the past but has since been removed.

Group related functionalities into their own packages or integrate them into the original package if they're not used by many others. This helps avoid the clutter of small packages with only one file.

Here's a rough guide to help you structure your project:

Project Layout

Project Layout is crucial for a well-structured project. A package's name should provide context for its contents, making it easier for clients to understand what the package is for and how to use it.

Don't use the pkg pattern, as it goes against the Go philosophy of naming packages that signify what they contain. It's a common standard used by many projects, but it's been removed from the Go project itself.

Same for util or misc packages - they're not recommended. Instead, break out related util functionalities into its own package or if it's not used by a lot of packages, make it part of the original package itself.

For your interest: Golang Go

Credit: youtube.com, How To Structure A Programming Project…

Too many small packages with only one file can be a sign of splitting packages without giving thought into them. Look at the API boundaries and group the packages into bigger chunks. This will make your project layout more organized and easier to maintain.

Here are some guidelines to keep in mind when structuring your packages:

  • Don't have too many small packages with only one file.
  • Break out related util functionalities into its own package.
  • Group packages into bigger chunks based on API boundaries.

Structures

Structures are a crucial part of a project's codebase, and understanding how to use them effectively can greatly improve the maintainability and readability of your code.

In Go, error structures are used to provide a way for callers to interrogate the error programmatically, rather than relying on string matching. This is especially important in production code, where distinguishing between different error conditions is crucial.

Error structures can be as simple as unparameterized global values, which can be compared to known error values using the == operator. However, if extra information is needed, it's better to present it structurally, like the os.PathError type, which includes the pathname of the failing operation in a struct field.

Credit: youtube.com, Keep your project structure simple!

You can also use other error structures, such as a project struct containing an error code and detail string, but be sure to use canonical codes.

Here are some benefits of using option structures:

  • The struct literal includes both fields and values for each argument, making them self-documenting.
  • Irrelevant or “default” fields can be omitted.
  • Callers can share the option struct and write helpers to operate on it.
  • Structs provide cleaner per-field documentation than function arguments.
  • Option structs can grow over time without impacting call-sites.

Option structures are particularly useful when all callers need to specify one or more options, or when a large number of callers need to provide many options.

Variadic options can provide even more benefits, such as options taking no space at a call-site when no configuration is needed, and options still being values that can be shared and written helpers for. However, using variadic options requires a substantial amount of additional code, so it should only be used when the advantages outweigh the overhead.

Coding Style

We use the Effective Go guide as our starting point for the style guide, so it's essential to familiarize yourself with its conventions.

Following the conventions outlined in Effective Go is the default unless explicitly mentioned otherwise.

For constants, it's worth noting that Effective Go recommends using MixedCaps, which is a departure from the common ALL_CAPS convention used in many languages.

If this caught your attention, see: Go vs Golang

Stylistic

Credit: youtube.com, Should I trust this code style guide?

In Go, we use descriptive names to make our code understandable and maintainable. A more descriptive name helps with code understanding and maintenance, and it's at very little cost, given the auto-complete feature in most IDEs and editors.

Avoid one-letter variable names, other than in the case of very well-known and widely understood conventions, such as using "i" for index in a loop.

If a name contains an acronym, only capitalize the first letter of the acronym. For example, use "someEksCluster" rather than "someEKSCluster".

Choose package names that lend meaning to the names they export. This means that the package name should be descriptive and help the user understand what the package does.

In table-driven tests, prefer to specify field names when initializing test case struct literals. This is helpful when the test cases cover a large amount of vertical space, when there are adjacent fields with the same type, and also when you wish to omit fields which have the zero value.

Credit: youtube.com, The purest coding style, where bugs are near impossible

Here are some examples of how to use descriptive names in Go:

In general, it's a good idea to use descriptive names that are easy to understand and follow the conventions outlined in the Effective Go guide. This will make your code more readable and maintainable, and will also make it easier for others to understand and work with your code.

Interfaces

Interfaces are a crucial part of coding style, and there are some key guidelines to keep in mind.

Interface names should end with “-er”, indicating that they are designed around the concept of “doing” something. This is just a guideline, but it's a helpful one to follow.

Try not to define interfaces on the implementer side of an API “for mocking”. Instead, design the API so that it can be tested using the public API of the real implementation.

Here are some key takeaways to keep in mind:

  • Return structs, accept interfaces.
  • Interface names should end with “-er”.
  • Don't define interfaces for mocking; design for testing.

By following these guidelines, you can write more effective and maintainable interfaces that make your code easier to work with.

Pass-by-Value vs Pass-by-Pointer

Credit: youtube.com, Pass by Value vs. Pass by Reference

In Go, passing arguments to a function is a bit more complex than in other languages. Every function operates on a copy of the arguments passed into the function. This is because even for types that are passed by pointer, like maps, slices, and channels, Go creates a copy of the pointer and passes that.

Primitive types like arrays, booleans, floats, integers, strings, and structs are passed as values, meaning their value is copied. This is different from languages that support pass-by-reference, where the function receives a reference to the original variable.

Maps, slices, and channels, on the other hand, are passed by pointer, but not in the way you might think. When you create an instance of one of these types, the Go language instantiates an internal struct and returns a pointer to it. So, when you pass one of these types to a function, you're passing a copy of the pointer, not the original pointer.

Credit: youtube.com, C Programming Tutorial 96 - Passing by Value vs Pointer

Here's a breakdown of how different types are passed to functions in Go:

This means that if you pass a map, slice, or channel to a function, and the function modifies the original variable, the changes will be reflected in the original variable. However, if you pass a primitive type to a function, any changes made to the function's copy of the variable will not affect the original variable.

It's worth noting that Go does not support pass-by-reference in the classical sense, where a function can modify a variable passed to it without making a copy of the variable. This is because Go does not have reference variables like C++ does.

Use String.Builder for piecemeal string construction

When building a string bit-by-bit, it's best to use strings.Builder. This approach takes amortized linear time, whereas using the "+" operator or fmt.Sprintf takes quadratic time when called sequentially.

You might be wondering why this matters. The difference in performance can add up quickly, especially when working with large strings.

A close-up of adults and child playing an educational string game on a vibrant yellow board.
Credit: pexels.com, A close-up of adults and child playing an educational string game on a vibrant yellow board.

Here's a brief rundown of the benefits of using strings.Builder:

  • Amortized linear time: This means that the time it takes to build a string using strings.Builder grows linearly with the size of the string.
  • Quadratic time with "+" and fmt.Sprintf: Using these methods can lead to slower performance, especially when building large strings.

In practice, using strings.Builder can make a big difference in the efficiency of your code. By choosing the right tool for the job, you can write more effective and efficient code.

Error Handling

Error handling is a crucial aspect of Go programming, and it's essential to handle errors deliberately. Errors are values that can be created by code and consumed by code, and they can be converted into diagnostic information for display to humans, used by the maintainer, or interpreted by an end user.

When creating an error value, consider giving it structure, and when handling an error, consider adding information that the caller and/or callee might not have. This can include details about why the error occurred and other relevant information.

Use the errors standard library package for handling single errors, and for operations that can produce multiple errors, leverage the MultiError package by accumulating all the errors into a single MultiError and returning that, rather than returning every error individually.

If this caught your attention, see: Golang Reflect to Call Function in Package

Credit: youtube.com, Todo dev Golang COMETE ESSES ERROS ⚠️

Here are some best practices for error handling:

  • Include the user input for validation errors.
  • Return errors instead of boolean for ID validation errors.
  • Program initialization errors should be propagated upward to main, which should call log.Exit with an error that explains how to fix the error.
  • Avoid using panic, and instead return errors to the caller.
  • Use custom error types to create your own types that implement the error interface so that error messages are clear and have well-defined types you can check against.

Errors

Errors are a crucial part of error handling in Go, and understanding how to handle them effectively is essential for writing robust code.

Errors are values that can be created by code and consumed by code, and they can be converted into diagnostic information for display to humans. They can also be used by the maintainer or interpreted by an end user.

When creating an error value, it's worth considering whether to give it any structure. This can help provide more context and make it easier for callers to handle the error.

Here are some key considerations for error handling:

  • When handling an error, consider adding information that you have but that the caller and/or callee might not.
  • Use custom error types to create your own types that implement the error interface so that error messages are clear and have well-defined types you can check against.
  • Include stack traces to make it easier to diagnose errors.
  • Include user input for validation errors to provide more context.
  • Return errors instead of boolean values for ID validation errors to provide more information.

In Go, it's generally a good idea to document significant error sentinel values or error types that your functions return to callers so that callers can anticipate what types of conditions they can handle in their code.

Credit: youtube.com, My FAVORITE Error Handling Technique

Here are some best practices for documenting errors:

  • Document whether the values returned are pointer receivers or not, as this can affect how errors are compared using errors.Is, errors.As, and package cmp.
  • Document overall error conventions in the package's documentation when the behavior is applicable to most errors found in the package.

By following these guidelines, you can write more effective error messages and make it easier for callers to handle errors in your code.

Slice Gotchas Guide

Assigning a new value to a parameter with = won't affect the argument in any way. This can lead to unexpected behavior, as shown in the example where trying to append a value to a slice resulted in printing [1], which is not what we want.

To avoid this issue, you need to explicitly define the parameter as a pointer type and pass it as such. This is because a slice is a pointer to an internal struct type, but it's an implementation detail that doesn't affect how you use it.

Modifying a slice will modify the original backing array, even if you're working with a slice of a slice. This means that if you're not careful, you can end up modifying the original array unexpectedly.

Asian man in white shirt focused on coding at modern office workspace.
Credit: pexels.com, Asian man in white shirt focused on coding at modern office workspace.

To avoid this, you can set the capacity of the slice when taking a slice of a to assign to b. This will cause a new backing array to be created for the b slice, so append will cause a new array to be allocated.

The 'fix' is to use the syntax b := a[:2:2], which sets the capacity of the b slice such that append will cause a new array to be allocated. This means a will not be modified, nor will the c slice of a.

Testing

Testing is an essential part of writing robust and maintainable Go code. Our goal is not to achieve 100% test coverage, but to have enough testing to give us confidence that our code works and allows us to update it quickly.

In terms of testing, we should consider the likelihood of bugs, the cost of bugs, and the cost of tests. This involves weighing the importance of testing different parts of our code, such as payment systems or security functionality, against the effort required to write and maintain tests.

Credit: youtube.com, Better table-driven tests for Go and Visual Studio Code

Tests should run in parallel whenever possible, which is done by including a call to t.Parallel in the test function.

To keep setup code scoped to specific tests, we should avoid calling setup functions from multiple test cases. Instead, we should call setup functions explicitly in test functions that need them, like mustLoadDataset in the example.

Here are some factors to consider when deciding where to focus our testing efforts:

  1. The likelihood of bugs: some code is harder to get right than others, and merits correspondingly more tests.
  2. The cost of bugs: some bugs are much more costly than others — e.g., bugs in payment systems or security functional

so it makes sense to invest more in testing there.The cost of tests: some types of tests are cheap and easy to write and maintain, whereas others are very costly and brittle.

Concurrency

In Go, users assume that read-only operations are safe for concurrent use and don't require extra synchronization.

Synchronous functions are preferred by default, as async calls can be hard to get right and introduce data races. Always measure and prove the need for asynchronous code before implementing it.

Documentation is crucial when working with concurrency. If it's unclear whether an operation is read-only or mutating, or if synchronization is provided by the API, documentation should be provided to clarify the semantics. This can be done in the type definition if the API provides synchronization in entirety.

Here's an interesting read: Rest Api with Golang

Concurrency

Close-up of a computer screen displaying colorful programming code with depth of field.
Credit: pexels.com, Close-up of a computer screen displaying colorful programming code with depth of field.

Concurrency is a crucial aspect of Go programming, and understanding its nuances can make a huge difference in your code's reliability and performance. Go users assume that read-only operations are safe for concurrent use and don't require extra synchronization.

However, mutating operations are a different story. They're not assumed to be safe for concurrent use and require the user to consider synchronization. This is especially true for operations that may not be immediately obvious as mutating, like cache lookups.

Documentation is key when it comes to concurrency. If it's unclear whether an operation is read-only or mutating, it's essential to document it clearly. A good example of this is the `Lookup` function in the `package rucache`. This function returns data from the cache, but it's not safe for concurrent use because it can mutate the LRU cache internally.

On the other hand, if an API provides synchronization, it's safe for simultaneous use by multiple goroutines. For instance, the `NewFortuneTellerClient` function in the `package fortune_go_proto` returns a client that's safe for concurrent use.

Credit: youtube.com, Threading Tutorial #1 - Concurrency, Threading and Parallelism Explained

Here are some guidelines to keep in mind when considering concurrency:

  • It's unclear whether the operation is read-only or mutating.
  • Synchronization is provided by the API.
  • The API consumes user-implemented types of interfaces, and the interface's consumer has particular concurrency requirements.

In general, prefer synchronous functions by default. Async calls can be tricky to get right and may introduce data races. If you think something needs to be asynchronous, measure it and prove it. Ask yourself questions like: Does it improve performance? What's the tradeoff of the happy path vs. slow path? How do I propagate errors? What about back-pressure? What should be my concurrency model?

Signal Boosting

Signal Boosting is a technique that can make your code more readable by highlighting the differences in a conditional statement. You can achieve this by adding a comment that draws attention to the difference.

A comment like "The comment draws attention to the difference in the conditional" can be very effective in making your code more understandable. This approach is demonstrated in the example from io/fs, where a comment is used to explain the difference in the conditional.

In the io/fs example, the function is rewritten with an option structure, which shows how signal boosting can be used to make the code more readable. This approach can make a big difference in how easy it is to understand your code.

Recommended read: Golang Io

Code Organization

Lines of Code
Credit: pexels.com, Lines of Code

In Go, it's common to create new packages under the pkg or internal directories. The internal directory contains code that we don't want to be consumed by other clients.

To decide which directory to use, follow this simple litmus test:

  • The internal directory contains code that we don't want to be consumed by other clients.
  • The pkg directory contains all packages that we want to make exportable to other clients.

This approach helps keep your code organized and makes it easier to manage dependencies between packages.

Util Packages

In Go, package names should be related to what the package provides.

Uninformative names like "util", "helper", or "common" are usually a poor choice because they can cause import conflicts if used too broadly.

Naming a package after what the callsite will look like can make the code easier to read.

Consider what the callsite will look like, and you can tell roughly what each package does, even without knowing the imports list.

With less focused names, packages like cloud.google.com/go/spanner/spannertest, io, and crypto/elliptic might read in a confusing way.

Import Ordering

Import Ordering is a crucial aspect of code organization. The Go programming language recommends grouping imports into two or more blocks in a specific order.

Close-Up Shot of a Person Taking Orders Through Order Terminal
Credit: pexels.com, Close-Up Shot of a Person Taking Orders Through Order Terminal

Standard library imports should come first, followed by imports from other packages. This order makes sense because standard library imports are typically used more frequently than imports from other packages.

Optional groups include Protobuf imports and Side-effect imports. These groups can be used to separate imports that have specific side effects or dependencies. However, using these groups requires attention from both authors and reviewers to ensure consistency.

The goimports tool can help with import ordering, but it only knows about the mandatory groups. This means that optional groups may be prone to invalidation by the tool.

Here's a summary of the recommended import ordering:

  • Standard library imports
  • Imports from other packages
  • (Optional) Protobuf imports
  • (Optional) Side-effect imports

Either approach is fine, but it's essential to avoid leaving imports in an inconsistent, partially grouped state.

Composite Literals

Composite literals are useful for declaring values when you know the initial elements or members. This approach can be more efficient than zero-value initialization.

You should use composite literals to declare values with initial elements or members. This is especially helpful when you need to create a value with specific properties.

Credit: youtube.com, Golang Composite Literal

Composite literals can be visually noisy if used to declare empty or memberless values. In these cases, zero-value initialization might be a better option.

If you need a pointer to a zero value, you have two options: empty composite literals and the new keyword. Both are acceptable, but the new keyword serves as a reminder that a non-zero value wouldn't work with a composite literal.

Best Practices

In Go, it's okay to "stomp" on a variable by reusing its name in a short variable declaration with the := operator, as long as the original value is no longer needed.

Be mindful of variable declarations and the potential for stomping, especially when working with short variable declarations.

This can save you from unnecessary variable creation, making your code more efficient.

Best Practices

Go Style Best Practices are essential for writing clean and efficient code. This is especially true when using short variable declarations with the := operator, where in some cases a new variable is not created, a phenomenon known as stomping.

Programming Code on Laptop Screen
Credit: pexels.com, Programming Code on Laptop Screen

Stomping is okay when the original value is no longer needed. This means you can reuse the variable name without worrying about overwriting important data.

In Go, it's okay to stomp when the original value is no longer needed. This can be a useful technique in certain situations, but use it judiciously to avoid confusion.

For more information on Go's best practices, check out the Go Tip #24: Use Case-Specific Constructions. This tip provides additional guidance on how to write effective Go code.

Suggest a New Rule

To propose a new rule, follow the process below.

To add a new rule, start by adding it to the agenda in the Server Guild meeting and propose it.

This ensures that the community is aware of the proposed change and can provide feedback.

If the proposed rule gets accepted, create a go-vet rule (if possible), or a golangci-lint rule to prevent new regressions from creeping in.

Woman in focus working on software development remotely on laptop indoors.
Credit: pexels.com, Woman in focus working on software development remotely on laptop indoors.

This helps maintain code quality and prevents issues from arising in the future.

Fix all existing issues related to the new rule.

This ensures that the codebase remains consistent and up-to-date.

Add the new rule to this guide once it's been implemented.

This helps keep the guide up-to-date and ensures that others can learn from the new rule.

Designing Extensible Validation APIs

Designing extensible validation APIs is crucial for a library's success, as it allows users to test their code against the library's requirements. This means providing facilities for users to write tests that ensure their code conforms to the library's standards.

Most Go tests have a typical structure, with the test function controlling the program flow, and it's recommended to keep it that way. The guidance on no assert and test functions encourages developers to maintain this structure.

To author support for these tests, you need to design test helpers in a way that's consistent with Go style. Test helpers are functions that perform test setup and cleanup, not common assertion facilities.

Functionality

Credit: youtube.com, Learning Golang: Functional Options / Default Configuration Values Pattern

In Go, you should prefer functions that take all their inputs as function parameters, rather than reading them via side effects like reading from disk or the network.

Functions should have their entire behavior focused on returning values, including errors, and avoid performing side effects like writing to disk or the network.

By centralizing side effecting code to a few isolated places, you can keep your logic clean and easy to reason about.

Function and Method Names

Function names should be noun-like, avoiding the prefix "Get". For example, a function that returns a job name should be named "JobName" rather than "GetJobName".

Using descriptive names is crucial for code understanding and maintenance. A one or two letter abbreviation of the type suffices as a method receiver name, such as "c" for "Client".

Functions that do something should be given verb-like names. For instance, a function that writes detail should be named "WriteDetail".

Credit: youtube.com, Function & Method Naming: Best Practices Explained!

Identical functions that differ only by the types involved should include the name of the type at the end of the name. For example, "ParseInt" and "ParseInt64" are good examples.

Here's a summary of function and method name conventions:

A more descriptive name helps with code understanding and maintenance, at very little cost. Avoid one-letter variable names, unless it's a well-known convention like "i" for index.

Return ID on Validation

Returning errors instead of boolean values can greatly improve the user experience. This is especially true when validating IDs, as the user will have no idea why their input is invalid.

Including the user input for validation errors can help clarify the issue. For example, instead of returning a generic error message, you can specify the exact input value that caused the error.

Returning errors instead of booleans also helps with logging, as you can log the specific object that is invalid. This can be particularly useful when debugging complex systems.

Including the user input in error messages helps users understand what went wrong and how to fix it. By doing so, you can reduce the number of support requests and improve overall user satisfaction.

Functional

Monochrome Shot of Go Kart Racers Standing beside each Other
Credit: pexels.com, Monochrome Shot of Go Kart Racers Standing beside each Other

Functional design is all about simplicity and ease of use. It's about creating products that are intuitive and straightforward, with a clear purpose.

A well-designed functional product can make a big difference in how users interact with it. For example, a simple and intuitive interface can reduce errors and increase user satisfaction.

The goal of functional design is to create a seamless user experience. This means minimizing clutter and unnecessary features, and focusing on the core functionality of the product.

Functional design can also improve accessibility. For instance, a product with a clear and consistent layout can be easier to use for people with disabilities.

A good example of functional design is a smartphone with a simple and intuitive interface. This allows users to quickly and easily access the features they need.

Functional design is not just about aesthetics, but also about creating a product that is easy to use and understand. By focusing on the core functionality of the product, designers can create a more user-friendly experience.

Complex Command-Line Interfaces

Credit: youtube.com, argparse: complex CLI made simple | Maxim Fedorov | Code BEAM V 2020

Complex command-line interfaces can be a challenge to design and implement. If you're building a program with sub-commands like kubectl, you'll want to consider using the subcommands package, as it's the simplest and easiest to use correctly.

For example, kubectl create, kubectl run, and other sub-commands are all provided by the program kubectl. There are at least two libraries in common use for achieving this: cobra and subcommands.

If you don't have a preference or other considerations are equal, subcommands is recommended. However, if you need different features that it doesn't provide, pick one of the other options.

Here are the two main libraries you can use:

  • cobra
  • subcommands

Note that cobra command functions should use cmd.Context() to obtain a context rather than creating their own root context with context.Background.

Documentation

Documentation is an essential part of any Go project. It provides a clear understanding of the code and its purpose, making it easier for developers to contribute and maintain the codebase.

Credit: youtube.com, GopherCon 2019: The Gopher's Manual of Style - Kris Brandow

The Go style guide recommends using Godoc formatting to format documentation. Specifically, a blank line is required to separate paragraphs.

This helps to improve readability and makes it easier for developers to understand the code. For example, the guide recommends using a blank line to separate the following paragraphs: "Good: func ExampleConfig_WriteTo() { ... }" and "See some/shortlink for config file format details."

The guide also provides examples of how to use Godoc formatting to create runnable examples and verbatim formatting. For instance, the following code is formatted verbatim: "Update runs the function in an atomic transaction. // This is typically used with an anonymous TransactionFunc: // if err := db.Update(func(state *State) { state.Foo = bar }); err != nil { // //... // }"

In addition to Godoc formatting, the guide recommends previewing the documentation before and during the code review process to ensure that the godoc formatting is rendered correctly.

Additional reading: Gcloud Api Using Golang

Documents

Credit: youtube.com, Writing effective documentation | Beth Aitman | #LeadDevBerlin

At Google, they have a set of documents that outline the foundation of Go style, which is used as the basis for style decisions and best practices.

The Style Guide is the most definitive document, and it's used to codify settled matters of Go style. It's intended to agree on a set of principles for weighing alternate styles and document the pros and cons of various style decisions.

These documents also aim to minimize surprises in Go readability reviews and help readability mentors use consistent terminology and guidance. They're not meant to be an exhaustive list of comments that can be given in a readability review or a list of all the rules everyone is expected to remember and follow at all times.

Here are the main goals of the Style Guide and related documents:

  • Agree on a set of principles for weighing alternate styles
  • Codify settled matters of Go style
  • Document and provide canonical examples for Go idioms
  • Document the pros and cons of various style decisions
  • Help minimize surprises in Go readability reviews
  • Help readability mentors use consistent terminology and guidance

It's worth noting that these documents are not meant to replace good judgment in the use of language features and style, and they're not intended to justify large-scale changes to get rid of style differences.

Logging

Credit: youtube.com, Documentation Log

Logging is a crucial aspect of documentation, and it's essential to get it right. Log messages should be annotated with contextual information in the form of key-value pairs to make it easier to identify the context they originated from.

Using snake_case for keys is a good practice, as it makes the log messages more readable and easier to understand. For example, if you're logging a message about a user's account, you might use a key-value pair like "user_id": 123.

Good log messages should clearly express what went wrong and help the maintainer by including relevant information to diagnose the problem. This is especially important when logging errors, as it can help the caller handle the issue and prevent logspam.

Here are some best practices to keep in mind when logging errors:

By following these best practices, you can create effective log messages that help maintainers diagnose and fix issues quickly.

Initialization

Program initialization errors should be propagated upward to main, which should call log.Exit with an error that explains how to fix the error. This approach is preferred over using log.Fatal, as it provides a more actionable message.

Prefer := over var when initializing a new variable with a non-zero value for consistency. This makes the code easier to read and understand.

For complex formatting, consider using text/template or safehtml/template as appropriate. This can help simplify the code and make it more maintainable.

Cleanup

Credit: youtube.com, Cleaning Up Ship Initialization Code

Cleanup is crucial to avoid resource leaks and bugs. Callers won't use the API correctly if they're unsure about what to do with the resources they've acquired.

Documenting explicit cleanup requirements is essential. This ensures that users of the API understand how to properly clean up after themselves.

If it's unclear how to clean up resources, explain the process. GoTip #110 reminds us not to mix exit with defer. This can lead to confusion and bugs.

Here are some key takeaways to keep in mind:

  • Document cleanup requirements clearly and explicitly.
  • Explain how to clean up resources if it's not immediately obvious.
  • Be mindful of mixing exit and defer, as this can lead to issues.

Program Initialization

Program initialization is a critical step in getting your application up and running. Program initialization errors should be propagated upward to main.

Bad flags and configuration can cause these errors, which should be explained to the user in a way that helps them fix the issue. This is where a well-crafted error message comes in – one that's generated by the program itself, not just a stack trace.

In such cases, log.Fatal should not be used because it won't provide a useful stack trace. A human-generated, actionable message is what the user needs to fix the error.

Testing and Validation

Credit: youtube.com, A hands-on guide for proper Unit Testing in Go!

Testing and validation are crucial aspects of writing robust and maintainable code in Go. When creating test helpers, consider including the user input for validation errors to provide meaningful feedback.

A better approach is to include the input value in the error message, as shown in the example: "invalid export type, must be one of: csv, actiance, globalrelay". This helps users understand what went wrong and how to fix it.

To create test helper packages, introduce a new Go package based on the production one for testing. A safe choice is to append the word "test" to the original package name. For instance, if you have a package called "creditcard", create a test package called "creditcardtest".

Go distinguishes between "test helpers" and "assertion helpers". Test helpers perform test setup and cleanup, while assertion helpers are common assertion facilities.

In a typical Go test, the test function controls the program flow, and the no assert and test functions guidance encourages you to keep it that way. This section explains how to author support for these tests in a way that is consistent with Go style.

Credit: youtube.com, Testing HTTP service in Go - Anton Klimenko

When test helpers fail, their failure often signifies that the test cannot continue. Prefer calling one of the Fatal functions in the helper to keep the calling side cleaner. The failure message should include a description of what happened.

Here are some tips for error handling in test helpers:

  • If a helper calls (*testing.T).Error or (*testing.T).Fatal, provide some context in the format string to help determine what went wrong and why.
  • If nothing a helper does can cause a test to fail, it doesn’t need to call t.Helper. Simplify its signature by removing t from the function parameter list.

In terms of testing, we don’t necessarily aim for 100% test coverage. Our goal is to have enough testing to give us confidence that our code works and allow us to update it quickly. When adding tests, keep in mind the following factors:

  1. The likelihood of bugs: some code is harder to get right than others, and merits correspondingly more tests.
  2. The cost of bugs: some bugs are much more costly than others — e.g., bugs in payment systems or security functional

so it makes sense to invest more in testing there.The cost of tests: some types of tests are cheap and easy to write and maintain, whereas others are very costly and brittle.

Unless there’s a reason not to, tests should run in parallel. This is done by including a call to t.Parallel in the test function.

Additional reading: Golang Testing Parallel

Code Style and Formatting

Credit: youtube.com, Idiomatic Go Naming Conventions (Golang)

Reduce indentation is key. This is an example from mlog/human/parser.go, which can be simplified.

Consistent spacing between lines is crucial for readability. This is achieved by using a consistent number of blank lines between functions.

Code should be formatted with a consistent number of spaces for indentation. This helps maintain readability and makes code easier to understand.

Avoid Repetition

Avoid Repetition is a crucial aspect of code style and formatting. It makes your code more readable and easier to understand.

Repeating the name of the package in a function name is unnecessary. For example, instead of `packageyamlconfigfuncParseYAMLConfig(inputstring)(*Config,error)`, simply use `packageyamlconfigfuncParse(inputstring)(*Config,error)`.

You can also omit the name of the method receiver from a method name. This is because it's already clear that the method is for the receiver. For instance, `func(c*Config)WriteConfigTo(wio.Writer)(int64,error)` can be simplified to `func(c*Config)WriteTo(wio.Writer)(int64,error)`.

Don't repeat variable names in function parameters. This makes the code cluttered and harder to read. For example, `funcOverrideFirstWithSecond(dest,source*Config)error` is better written as `funcOverride(dest,source*Config)error`.

Credit: youtube.com, COMP1 - Efficiency #2 - avoiding repetition of code

Similarly, you don't need to repeat the names and types of the return values. This is because the return values are already defined in the function signature. So, `funcTransformToJSON(input*Config)*jsonconfig.Config` can be shortened to `funcTransform(input*Config)*jsonconfig.Config`.

To disambiguate functions with similar names, it's acceptable to include extra information. This helps prevent confusion and makes the code more maintainable.

Minimize ToJSON Methods

Creating multiple ToJSON methods for model structs can lead to bugs and inefficiencies. It's better to use json.Marshal directly at the call site.

This approach avoids suppressing the JSON error, which has caused issues in the past. It also eliminates the need for a double conversion from byte-slice to string to byte-slice again.

If you have multiple return statements in an if-else statement, consider removing the else block and outdenting it. This can make your code more readable and efficient.

Here are some benefits of avoiding ToJSON methods:

  • It avoids bugs due to the suppression of the JSON error.
  • It eliminates the need for a double conversion from byte-slice to string to byte-slice again.

Reduce Indentation

Reducing indentation can make your code more readable and easier to maintain.

Credit: youtube.com, Code style 8 space indent hard tabs 80 columns max

For example, in the mlog/human/parser.go file, a block of code can be simplified by removing unnecessary indentation.

This can be achieved by removing extra whitespace characters, making the code more concise and efficient.

In the example, the simplified code is more readable and easier to understand, which is essential for any programming project.

By reducing indentation, you can improve the overall quality of your code and make it more maintainable in the long run.

Claire Beier

Senior Writer

Claire Beier is a seasoned writer with a passion for creating informative and engaging content. With a keen eye for detail and a talent for simplifying complex concepts, Claire has established herself as a go-to expert in the field of web development. Her articles on HTML elements have been widely praised for their clarity and accessibility.

Love What You Read? Stay Updated!

Join our community for insights, tips, and more.