Creating simple self documenting types in Go
Tags: go, programming
Published: 2022-05-14 17:19:49

Go has a rich type system, we can leverage it to better express ourselves in code and not resort to using things like comments or documentation.

for example consider the following composite type:

```go
type member struct {
    id   string
    img  string
    desc string
}
```

Here we have a `member` which is a struct with three fields: `id`, `img` and `desc` all of which are defined as strings while this works and the code compiles there are a few issues:

* All three fields have type string, but they most likely expect different formats.

* One can assign any arbitrary string to any of the fields (which is very easy to do by mistake).

* You might need to validate incoming data in more than one place before creating a `member` struct.

That piece of code is a classic example of a code "smell" known as primitive obsession, code smells are abstract signs or symptoms of software rot, primitive obsession is one of them, in a small application or a simple command line tool a struct def like that is fine but in a bigger program (think tens of thousands of lines of code) it becomes a real issue.

we can add types that help express our intentions better:

```go
type member struct {
    id   uuidStr
    img  urlStr
    desc text150
}

type (
  uuidStr string
  urlStr  string
  text150 string
)
```

We defined three new types: `uuidStr`, `urlStr` and `text150` all of them have `string` as their underlying types, our `member` struct fields describe the string format they use better.

we can take this further and create maker functions:

```go
func makeUuidStr(rawId string) (uuidStr, error) {
  if !canBeUUID(rawId) {
      return "", fmt.Errorf("\"%s\" is not a valid UUID string", rawId)
  }

  return uuidStr(rawId), nil
}

func makeUrlStr(rawURL string) (urlStr, error) {
  if !canBeURL(rawURL) {
      return "", fmt.Errorf("\"%s\" is not a valid URL", rawURL)
  }

  return urlStr(rawURL), nil
}

func makeText150(rawText string) (text150, error) {
  if len(rawText) > 150 {
    return "", fmt.Errorf("\"%s\" is over 150 bytes long!", rawText[:25])
  }

  return text150(rawText), nil
}
``` 

With the help of these functions we can then write a constructor for member:

```go
func createMember(rawId, rawImgURL, rawDesc string) (member, error) {
  id, idErr := makeUuidStr(rawId)
  img, imgErr := makeUrlStr(rawImgURL)
  desc, descErr := makeText150(rawDesc)

  if idErr != nil {
    return member{}, idErr
  } else if imgErr != nil {
    return member{}, imgErr
  } else if descErr != nil {
    return member{}, descErr
  }

  return member{id, img, desc}, nil
}
```

This may seem like a lot of work, but we've essentially made sure that our data is validated at construction time, we don't need to do any validation after that unless we want to update a field, our member will always be in a valid state, with that said there are things to take into account:

* If you plan to use custom types instead of primitives you need to use their constructors, and not use type casting or untyped constants as a quick and dirty way to work around the validation.

* To fix primitive obsession you traditionally would use classes (there would be a UUID class, a URL class, a Text class ... and so on) in go we don't have those, but some people would model `uuidStr` as a struct, with one or more methods to unwrap the string value, this complicates our design but it provides better protection against bugs, for example you can still do things like slicing or indexing a `uuidStr` or even concatenate two `uuidStr` values together.