Intermediate golang tricks and conventions Golang websrv

Some useful techniques for intermediate go user.Intermediate means here that the syntax and general concepts of the language are known.

Returning errors from deferred functions

It is best practise to defer e.g. cleanup operations like closing files:

func doSomething(filepath string) error{
  f, err := os.Open(filepath)
  if err != nil{
    return fmt.Errorf("error opening file %s: %w",filepath,err)
  }
  defer f.Close()
  // do something with the file
  return nil
}

One big downside of this most simple approach is that errors from f.Close() will get lost. Also, just logging them is not super helpful asthe errors might affect the business logic (e.g. a file might not have been saved correctly if closing fails after writing).

However, with named return values one can return the error from the closing operation together with other errors from the function:

func doSomething(filepath string) (retErr error) {
	f, err := os.Open(filepath)
	if err != nil {
		return fmt.Errorf("error opening file %s: %w", filepath, err)
	}
	defer func() {
	        retErr = errors.Join(retErr, f.Close())
	}()
	// do something with the file
	return nil
}

Implementing generic functions for pointer-receiver

Using generics together with implementations that have only a pointer-receiver can be a bit cumbersome and requires some tricks with type constraint.The following example uses a simple SetValue-interface as an example to construct a helper function setup which initializes the given generic typeand calls the method from the interface after initialization.

The go comments should explain how using generics works here:

// SetValue is the interface we want to wrap in a generic utility function `setup` defined below
type SetValue[TSet any] interface {
	SetValue(a TSet)
}

// SetValueStruct is our example implementation for the `SetValue` interface.
// It is also generic and will have only a pointer-receiver implementation as a value-receiver would not make sense for `SetValue`.
type SetValueStruct[TSet any] struct {
	val TSet
}

// ensure interface is implemented at compile time
var _ SetValue[int] = &SetValueStruct[int]{}

func (o *SetValueStruct[TSet]) SetValue(val TSet) {
	o.val = val
}

// SetValuePtr is a helper type constraint (not a real interface, they just share the same keyword in go).
// It ensures that the given type is a pointer that implements the `SetValue` interface over the generic `TSet` type.
type SetValuePtr[T any, TSet any] interface {
	*T
	SetValue[TSet]
}

// The helper function we want to implement.
// The generic signature looks a bit scary. The main constraint is via `TPtr` but as this type constraint
// is generic it has to depend on other generic types which we keep unconstraint as `any`.
// Usage is rather simple as by providing the first tpye `T` the others can be inferred.
func setup[T any, TSet any, TPtr SetValuePtr[T, TSet]](val TSet) T {
	result := TPtr(new(T))
	result.SetValue(val)
	return *result
}

func main() {
	o := setup[SetValueStruct[int]](2)
	fmt.Println(o)
}

Functional optional arguments

While this is a funny way to illustrate what can be modelled in go, for most libraries I would not recommend to use this pattern.The main reason against it is that it makes it hard to pass the arguments around. Usually having a simple Options-Struct as an argument is thesimpler and better maintainable approach.

Assume we have a struct that has a http.RoundTripper to make some HTTP-calls.We now want to give the user a convenient way to hand us a base http.RoundTripper our implementation will use.If none is provided we will use http.DefaultTransport..

Functional arguments provide a simple interface for such problems. Especially, when multiple optional parameters are present.

With them the usage becomes:

	// use default from provider
	client := mylib.New()
	// use custom http.RoundTripper
	client := mypackage.New(mylib.WithTransport(myTransport))

The actual implementation that enables this is:

type Opt func(*Client)

func WithTransport(transport http.RoundTripper) Opt {
	return func(c *Client) {
		c.Transport = transport
	}
}

type Client struct {
	Transport http.RoundTripper
}

func New(opts ...Opt) *Client {
	// set initial defaults
	c := &Client{
		transport: http.DefaultTransport,
	}
	// apply user provided overrides
	for _, opt := range opts {
		opt(c)
	}
	// further setup (ommitted)
	return c
}

Custom error types

This is more fundamental but still very relevant. Let's say we want to return a HttpStatusError that contains the received errorin a way that it can be used for further evaluation.

// compile time and IDE-check that we implement the error interface
var _ error = &HttpStatusError{}

type HttpStatusError struct {
	Code int
}

func (e *HttpStatusError) Error() string {
	return fmt.Sprintf("Unexpected HTTP status code: %d", e.Code)
}

Now we can use this error code from a function which just returns the error interface via:

import (
	"errors"
	"http"
)

func main() {
	err := someHttpFunc()
	var httpErr *HttpStatusError
	if errors.As(err, &httpErr) {
		// if errors.As returns true we know err is a HttpStatusError and the httpErr will be populated
		if httpErr.Code == http.StatusUnprocessableEntity {
			// do some special case handling
		}
	}
}

Testing for the error type via errors.As is compatible with errors.Join discussed at the beginning of this page and also error wrapping, e.g.:

  // to have unwrapping work it has to be invoked via the %w formatting directive
  return fmt.Errorf("Unexpected http err: %w", httpErr)