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
Celsiusbuilt onfloat64 - 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
Celsiusand the other isKilograms, 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:
- The
typekeyword, custom types and type aliases. - How to add methods to your own types.
- What a method set is, and the difference between value and pointer methods.
- The rules for which methods are available.
- 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
Marksis still a slice:len(m)andrangework 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
Grademake 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
RollNoandintmix freely, because they are one type.- Go's own
byteis an alias foruint8, andruneforint32.
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
*Counteronly. - Not in the method set of
Counter.
Method Availability Rules
| Type | Value 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
Tvalue 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
visitorsis aCounter, 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]
ReadWriterneeds every method ofReaderandWriter.*Notebookhas both, so it fits all three interfaces.- Go's own
io.ReadWriteris 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
announceonly reads, so it asks for aReader, not aReadWriter.- 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
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.
- Type alias =
type RollNo = int. Same type, another name. Method set = the methods a type has.
Thas value methods.*Thas value and pointer methods.- Calls on variables get automatic
&. Interfaces do not.
Embedded interfaces =
type ReadWriter interface { Reader; Writer }.- Ask for the smallest interface you need.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
Adding Celsius and Kilograms | Both are float64 underneath | They are different types | mismatched types |
type RollNo = int then adding a method | Aliases are new types | An alias of int is int | cannot define new methods on non-local type |
| Putting a value in an interface that needs pointer methods | Go adds & for you | Only for direct calls | method has pointer receiver |
Using a plain string for grades everywhere | Strings are fine | A Grade type documents and checks | Typos like "a" slip through. |
| Giant interfaces | One big interface is simpler | Small ones combine better | Few types can fit it. |
| Methods on a type from another package | You can extend any type | Only types defined in your package | A build error. Define your own type. |
Check Yourself
Exercises
What to Remember
type X Ymakes a new type with its own meaning. Go will not mix it up.type X = Yis only another name for the same type.- Any type defined in your package can have methods.
*Thas all methods.Thas 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.