Skip to content

Instantly share code, notes, and snippets.

@SamWhited
Last active December 21, 2020 12:21
Show Gist options
  • Save SamWhited/6cdbc49b4562e1a1b0526af523f5c5d7 to your computer and use it in GitHub Desktop.
Save SamWhited/6cdbc49b4562e1a1b0526af523f5c5d7 to your computer and use it in GitHub Desktop.
Experience report about attempting to ensure compile time correctness of values using the type system

Faking Enumeration Types with Consts and Unexported Types

What I Wanted to Do

While working on an implementation of the wire protocol from a network standard, I needed to implement an error response. These errors may come in one of several flavors which is encoded and sent along with the error:

  • Cancel
  • Auth
  • Continue
  • Modify
  • Wait

It would be invalid to send an error with a type that is not in this list, so I wanted to provide the user who would be constructing this error response with an API for easily sending valid errors that would ensure.

What I Did

To ensure correctness as much as possible, I created an unexported type and a list of constants to represent the various possible error conditions:

type errorType int

const (
	// CancelError indicates that the error cannot be remedied and the operation
	// should not be retried.
	CancelError errorType = iota

	// AuthError indicates that an operation should be retried after providing
	// credentials.
	AuthError

	// ContinueError indicates that the operation can proceed (the condition was
	// only a warning).
	ContinueError

	// ModifyError indicates that the operation can be retried after changing the
	// data sent.
	ModifyError

	// WaitError is indicates that an error is temporary and may be retried.
	WaitError
)

This type can then be included in the struct representing errors and users of the library cannot directly create new values with type errorType:

// mylib

type Error struct {
	Type errorType `xml:"type,attr"`
}
// main

err := mylib.Error{
	Type: mylib.errorType(10), // Does not compile, unexported type!
}

What Went Wrong

Users of the library can create their own values (which I have to special case in the code that marshals/unmarshals errorType values) by performing operations on existing values:

oops := WaitError + 1
fmt.Printf("%T: %v\n", oops, oops)

// Output: mylib.errorType: 5

Playground

This could easily be done accidentally if they did not understand the API and, eg. thought that AuthError + ModifyError meant that you needed to be authenticated and modify the data, or if they just mixed up to variables and accidentally added something to a value of type errorType.

Currently there is no way to ensure correctness using only the type system at compile time: runtime checks are required. In this particular case it is not clear to me that there is a good way to handle this at runtime. The options are to return an error, panic, or to serialize the invalid data to a "default" type which may not be semantically correct in the context of the wire protocol. Having this fail at runtime also means end users of the network service using the application developed using my library are likely to be the ones that first notice an issue, this may be multiple people removed from the developer who made the mistake and created an invalid errorType value.

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