Search code examples
gotype-definition

When to use type definition for basic types


My question is about the type definition in Go and when to use it for basic types.
Consider the following example.
I have this struct which represents a row from my Database:

type DBEntityAttribute struct {
    Id              uint64
    EntityId        uint64
    EndPointNumber  uint8
    AttributeNumber uint8
    ParentId        uint64
    Value           string
    Tag             int
    ContentType     sql.NullString
    Maturity        int
    Author          int
    AttributeType   string
    IsNegated       bool
}

The EntityId and AttributeNumber are properties I use in A LOT of other structs everywhere in the code.
Now I wanted to refactor it to something like this:

type EntityId uint64

type AttributeNumber uint8

type DBEntityAttribute struct {
        Id              uint64
        EntityId        EntityId
        EndPointNumber  uint8
        AttributeNumber AttributeNumber
        ParentId        uint64
        Value           string
        Tag             int
        ContentType     sql.NullString
        Maturity        int
        Author          int
        AttributeType   string
        IsNegated       bool
    }

This would allow me to change the type of the EntityId and AttributeNumber at one single place. Also, when I pass for example an entityId as a function parameter, I can now give it the specific EntityId type, instead of the anonymous uint64.

My question is now:

  • is this considered good Go code?
  • where should I stop, meaning should I declare a distinct type for every property that is used elsewhere in the code? Or only for the important ones?

I know this is probably subjective, but often the Go community agrees on certain patterns and I want to comply with their guidelines.


Solution

  • There's no hard and fast rule about when to use a custom type. My personal rule would be:

    Use a custom type when there's a reason.

    Your reason, of wanting to change the underlying type in a single location seems like a good reason.

    Other good reasons would be:

    • To assign a custom JSON (un)marshaler, or other interface method
    • As documentation
    • In conjunction with constants, to act as an enum
    • Improved type safety (avoid accidentally assigning a FooID to a BarID)