Advanced Type Design

By Syncfusion®, Inc. · Last updated

Custom types with the type keyword, type aliases, methods on any named type, method sets and their rules, and composing interfaces from smaller ones.

What Is Type Design?

Type design = choosing and building the right types for your program's data.

So far you have used Go's types (int, string) and your own structs. This lesson adds three more tools:

  • custom types: a new type built on an existing one, like Celsius built on float64
  • method sets: the rules for which methods a type and its pointer have
  • embedded interfaces: bigger interfaces built from smaller ones

Simple lesson: good types make wrong code hard to write.

A Real-World Example

The school nurse keeps two numbers for every student:

  • body temperature, in °C, like 37.5
  • weight, in kg, like 37.5

Both are numbers. But adding a temperature to a weight makes no sense.

Now the mapping:

  • If both are just float64, Go cannot stop the mix-up.
  • If one is Celsius and the other is Kilograms, Go refuses to add them.
  • The types carry the meaning, and Go checks it.

This lesson builds on three earlier lessons:

  • types and conversion from Data Types
  • methods and receivers from Structs and Methods
  • interfaces from Interfaces and Polymorphism

What You Will Learn

In this lesson, you will learn:

  1. The type keyword, custom types and type aliases.
  2. How to add methods to your own types.
  3. What a method set is, and the difference between value and pointer methods.
  4. The rules for which methods are available.
  5. How to build interfaces from smaller interfaces.

Type Definitions

type Keyword

Type definition = type NewName BaseType. It makes a brand-new type.

  • The new type has the same shape as the base type.
  • But Go treats it as a different type.
  • You can add methods to it.
type Celsius float64
type Kilograms float64
type Grade string
type Marks []int

Custom Types

Go stops the mix-up

package main

import "fmt"

type Celsius float64
type Kilograms float64

func main() {
	var temp Celsius = 37.5
	var weight Kilograms = 37.5
	fmt.Println(temp + weight)
}
  • Go refuses to build: mismatched types Celsius and Kilograms.
  • That mistake is caught before the program ever runs.

To mix them on purpose, convert: float64(temp) + float64(weight). See Data Types.

Methods on Custom Types

Any type you define in your package can have methods, not only structs.

Methods on a float and a slice type

package main

import "fmt"

type Celsius float64

func (c Celsius) Fahrenheit() float64 {
	return float64(c)*9/5 + 32
}

func (c Celsius) HasFever() bool {
	return c >= 38
}

type Marks []int

func (m Marks) Average() float64 {
	total := 0
	for _, x := range m {
		total += x
	}
	return float64(total) / float64(len(m))
}

func main() {
	t := Celsius(38.5)
	fmt.Println(t.Fahrenheit(), t.HasFever())

	m := Marks{80, 90, 70}
	fmt.Println(m.Average(), len(m))
}
101.3 true
80 3
  • Marks is still a slice: len(m) and range work on it.
  • But now it also has an Average() method.

Named Constants for a Custom Type

A Grade type with fixed values

package main

import "fmt"

type Grade string

const (
	GradeA Grade = "A"
	GradeB Grade = "B"
	GradeC Grade = "C"
)

func (g Grade) Message() string {
	switch g {
	case GradeA:
		return "Excellent"
	case GradeB:
		return "Good"
	default:
		return "Keep practising"
	}
}

func main() {
	var g Grade = GradeB
	fmt.Println(g, g.Message())
}
B Good
  • Functions that take a Grade make the meaning clear: func Award(g Grade).
  • You can also number such constants with iota. See Variables and Constants.

Type Aliases

Type alias = type NewName = ExistingType. It is just a second name for the same type.

Definition vs alias

An alias is the same type

package main

import "fmt"

type RollNo = int

func main() {
	var r RollNo = 7
	var n int = r
	fmt.Println(n + 1)

	var b byte = 65
	var u uint8 = b
	fmt.Println(u)
}
8
65
  • RollNo and int mix freely, because they are one type.
  • Go's own byte is an alias for uint8, and rune for int32.

Simple lesson: type X Y makes a new type with its own meaning, and type X = Y is only a second name.

Method Sets

Method set = the list of methods that a type has.

It decides which interfaces a type fits.

Value Methods

Value method = a method with a value receiver: func (c Counter) Show().

  • It is in the method set of Counter.
  • It is also in the method set of *Counter.

Pointer Methods

Pointer method = a method with a pointer receiver: func (c *Counter) Inc().

  • It is in the method set of *Counter only.
  • Not in the method set of Counter.

Method Availability Rules

TypeValue methods (t T)Pointer methods (t *T)
T✓✗
*T✓✓
  • When calling on a variable, Go helps: c.Inc() becomes (&c).Inc() automatically.
  • When fitting an interface, Go does not help: a T value does not have the pointer methods.

Calling both kinds on a variable

package main

import "fmt"

type Counter int

func (c *Counter) Inc() {
	*c++
}

func (c Counter) Show() string {
	return fmt.Sprint("count = ", int(c))
}

func main() {
	var visitors Counter
	visitors.Inc()
	visitors.Inc()
	fmt.Println(visitors.Show())
}
count = 2
  • visitors is a Counter, not a pointer.
  • visitors.Inc() still works, because Go takes its address for you.

Now an interface:

type Incrementer interface {
	Inc()
}

var c Counter
var i Incrementer = c   // error: Counter does not implement Incrementer
                        //        (method Inc has pointer receiver)
var j Incrementer = &c  // OK: *Counter has Inc
  • Store a pointer in the interface when its methods have pointer receivers.

In this lab, the runner does not report the "pointer receiver" error above.

The real go tool does. That is why it is shown as a plain block. Always use &value when an interface needs pointer methods.

Simple lesson: *T has every method, T has only the value methods, and interfaces follow these rules strictly.

Embedded Interfaces

Interface Composition

Interface composition = building an interface by listing other interfaces inside it.

Reader + Writer = ReadWriter

package main

import "fmt"

type Reader interface {
	Read() string
}

type Writer interface {
	Write(s string)
}

type ReadWriter interface {
	Reader
	Writer
}

type Notebook struct {
	pages []string
}

func (n *Notebook) Write(s string) {
	n.pages = append(n.pages, s)
}

func (n *Notebook) Read() string {
	return fmt.Sprint(n.pages)
}

func main() {
	var rw ReadWriter = &Notebook{}
	rw.Write("Maths homework")
	rw.Write("Science project")
	fmt.Println(rw.Read())
}
[Maths homework Science project]
  • ReadWriter needs every method of Reader and Writer.
  • *Notebook has both, so it fits all three interfaces.
  • Go's own io.ReadWriter is built exactly like this.

Reusable Contracts

Contract = an interface seen as a promise: "this type can do these things".

  • Small contracts: Reader, Writer, Closer.
  • Combine them as needed: ReadWriter, ReadCloser, ReadWriteCloser.
  • A function asks for only what it needs.

Ask only for what you need

package main

import "fmt"

type Reader interface {
	Read() string
}

type Notice struct {
	Text string
}

func (n Notice) Read() string {
	return n.Text
}

func announce(r Reader) {
	fmt.Println("Announcement:", r.Read())
}

func main() {
	announce(Notice{"Sports day on Friday"})
}
Announcement: Sports day on Friday
  • announce only reads, so it asks for a Reader, not a ReadWriter.
  • More types can then use it.

Easy way to remember

  • type X Y = new type, new meaning, can have methods.
  • type X = Y = same type, new name.
  • Pointer methods → put a pointer in the interface.
  • Small interfaces, combined when needed.

Simple lesson: build big interfaces from small ones, and let functions ask for the smallest one they need.

TL;DR

  1. Type definition = type Celsius float64. A new type with its own meaning.

    • Go will not mix it with other types without conversion.
    • Can have methods, even on numbers and slices.
  2. Type alias = type RollNo = int. Same type, another name.
  3. Method set = the methods a type has.

    • T has value methods. *T has value and pointer methods.
    • Calls on variables get automatic &. Interfaces do not.
  4. Embedded interfaces = type ReadWriter interface { Reader; Writer }.

    • Ask for the smallest interface you need.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Adding Celsius and KilogramsBoth are float64 underneathThey are different typesmismatched types
type RollNo = int then adding a methodAliases are new typesAn alias of int is intcannot define new methods on non-local type
Putting a value in an interface that needs pointer methodsGo adds & for youOnly for direct callsmethod has pointer receiver
Using a plain string for grades everywhereStrings are fineA Grade type documents and checksTypos like "a" slip through.
Giant interfacesOne big interface is simplerSmall ones combine betterFew types can fit it.
Methods on a type from another packageYou can extend any typeOnly types defined in your packageA build error. Define your own type.

Check Yourself

Exercises

What to Remember

  • type X Y makes a new type with its own meaning. Go will not mix it up.
  • type X = Y is only another name for the same type.
  • Any type defined in your package can have methods.
  • *T has all methods. T has only value methods. Interfaces follow this strictly.
  • Build big interfaces from small ones, and accept the smallest you need.

Next: Generics. You will learn how to write one function or type that works with many types, safely.