Skip to content

Instantly share code, notes, and snippets.

@haani-niyaz
Last active February 10, 2022 07:02
Show Gist options
  • Save haani-niyaz/feee031ec571f0f0ca42f2e585e5985b to your computer and use it in GitHub Desktop.
Save haani-niyaz/feee031ec571f0f0ca42f2e585e5985b to your computer and use it in GitHub Desktop.
Go application development guiding principles

Go Dev Guiding Principles

An opinionated list of practices when developing in Go.

(1) Names

Names should be self revealing.

When someone sees this variable in isolation, without seeing how it is being used, any documentation for it or any code that uses the variable, how closely can they guess the purpose of variable?

(2) Sane defaults

Configuration options are nice but consider the usability from the caller's point of view. If there is enough information to predict how it might be used (or how it may not be used) provide a sensible default.

Always try to provide sane defaults, even if those defaults aren’t terribly useful in the real world. This provides significant benefits for anyone attempting to debug, test, or simply play with your your code.

(3) Abstractions

The purpose of providing abstractions in your code is to hide complexity but not to be vague. Think carefully before introducing abstractions that dilute the usability. Abstractions should be distilled based on obvious patterns for reuse or when you've had enough experience with a similar problem domain to know in advance why an abstraction will be suitable.

As per https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction:

duplication is far cheaper than the wrong abstraction

Rules:

  1. Duplicate until you really understand your problem domain to provide a suitable abstraction.
  2. High degree of general purpose methods may not always be a good idea. For instance the level of abstraction required for a shared library vs a microservice is vastly different. For an app, special purpose methods or somewhat general purpose methods might be highly suitable over something that is trying to solve non existent problems.

(4) Fail visibly

Avoid terminating within a library and defer it to the caller (usually in main). A library should not have authority to crash a program. A library should pass the responsibility to the caller to decide how to proceed. This also has the benefit of the program being designed to terminate from one place, guaranteeing the developer that that they don't need to wade through the code or trace the log messages (if any) to understand why the program unexpectedly terminated.

(5) Sharing

Taken from the "Readability" section in the following article:

https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-escape-analysis.html

Given the following:

func createUserV2() *user {
     u := &user{
         name:  "Bill",
         email: "bill@ardanlabs.com",
     }

     return u
}

If you just focus on the return statement it would seem that a copy of u is passed up the call stack.

If you do this instead:

func createUserV2() *user {
     u := user{
         name:  "Bill",
         email: "bill@ardanlabs.com",
     }

     return &u
}

The return now tells you that u is being shared up the call stack and therefore escaping to the heap. The fact that you are sharing becomes explicit when you use return &u.

As mentioned in the article:

Remember, pointers are for sharing and replace the & operator for the word “sharing” as you read code

(6) Setting default values

  • Anonymous functions can be used as a mechanism to encapsulate logic to determine if the default value or caller provided value is accepted.
  • Use a constant to define the default values as this will aid developers to instinctively determine how default values are documented and where to go to check them. This is however only useful if precise names are used so there is no ambiguity.

Usage

package main

import (
	"fmt"
)

// defaultAvatarID represents the generic avatar used if the user doe not specify one.
const defaultAvatarID = 101

type user struct {
	name     string
	avatarID int32
}

// NewUser creates a new user.
//
// If the avatarID is nil, the default avatar provided by the system is used.
func NewUser(name string, avatarID *int32) *user {

	return &user{
		name: name,
		// If user has not set the avatarID, use the system provided one.
		avatarID: func() int32 {
			if avatarID == nil {
				return defaultAvatarID
			}
			return *avatarID
		}(),
	}
}

func PtrInt32(v int32) *int32 { return &v }

func main() {

	// Caller accepts default value
	u1 := NewUser("foo", nil)
	fmt.Println(u1)
	// Force the caller to explictly provide a value
	u2 := NewUser("bar", PtrInt32(101))
	fmt.Println(u2)
}

https://goplay.space/#SPHjm_L5cn4

(7) Constructor pattern

The constructor pattern makes the API simpler as the caller will most likely be intuitively familiar with how to begin using your API. This is suitable for complex instance construction or when unexported types logically makes sense to be confined within a newly created instance.

Sanitize input in constructors

Input provided by a caller should be validated to know that it is safe for use going forward. This enables you to fail visibly and early.

(8) Exported vs unexported

Start with unexported types, specially if constructors are in use. This will proactively prompt you to think about what the caller needs and export only what is required. Keeping the public API to a minimum will promote better usability as the caller does not need to guess or read documentation or worse, try to understand how it is used in the code.

(9) Provide error context

When returning errors consider what it means to the caller; Tell them why something failed if an implicit context does not exist.

An example of this is when you have a function that makes a get request and the API response only contains the default HTTP status code but no message body containing the context. Wrap these types of errors with some form of context so the caller can easily discern the reason for failure.

Example

See https://github.com/tomarrell/wrapcheck#tldr

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment