One of the main reasons I embrace Golang is its simple and concise type system. It follows the principle of least surprise and as per Rob Pike these design choices were largely influenced by the prior experiences.
In this post, I will discuss some of the main concepts which are essential in understanding Golang's type system.
Pre-declared Types
Golang by default includes several pre-declared boolean, numeric and string types. These pre-declared types are used to construct other composite types, such as array, struct, pointer, slice, map and channel.
Named vs Unnamed Type
A type can be represented with an identifier (called type name) or from a composition of previously declared types (called type literal). In Golang, these two forms are known as named and unnamed types respectively.
Named types can have their own method sets. As I explained in a previous post, methods are also a form of functions, which you can specify a receiver.
type Map map[string]string
//this is valid
func (m Map) Set(key string, value string){
m[key] = value
}
//this is invalid
func (m map[string]string) Set(key string, value string){
m[key] = value
}
You can define a method with named type Map
as the receiver; but if you try to define a method with unnamed type map[string]string
as the receiver it's invalid.
An important thing to remember is pre-declared types are also named types. So int
is a named type, but *int
or []int
is not.
Underlying Type
Every type do have an underlying type. Pre-declared types and type literals refers to itself as the underlying type. When declaring a new type, you have to provide an existing type. The new type will have the same underlying type as the existing type.
Let's see an example:
type Map map[string]string
type SpecialMap Map
Here the underlying type of map[string]string
is itself, while underlying type of Map
and SpecialMap
is map[string]string
.
Another important thing to note is the declared type will not inherit any method from the existing type or its underlying type. However, method set of an interface type and elements of composite type will remain unchanged. Idea here is if you define a new type, you would probably want to define a new method set for it as well.
Assignability
type Mystring string
var str string = "abc"
var my_str MyString = str //gives a compile error
You can't assign str
to my_str
in the above case. That's because str
and my_str
are of different types. Basically, to assign a value to a variable, value's type should be identical to the variable's type. It is also possible to assign a value to a variable if their underlying types are identical and one of them is an unnamed type.
Let's try to understand this with a more elaborative example:
package main
import "fmt"
type Person map[string]string
type Job map[string]string
func keys(m map[string]string) (keys []string) {
for key, _ := range m {
keys = append(keys, key)
}
return
}
func name(p Person) string {
return p["first_name"] + " " + p["last_name"]
}
func main(){
var person = Person{"first_name": "Rob", "last_name": "Pike"}
var job = Job{"title": "Commander", "project": "Golang"}
fmt.Printf("%v\n", name(person))
fmt.Printf("%v", name(job)) //this gives a compile error
fmt.Printf("%v\n", keys(person))
fmt.Printf("%v\n", keys(job))
}
Here both Person
and Job
has map[string]string
as the underlying type. If you try to pass an instance of type Job
, to name
function it gives a compile error because it expects an argument of type Person
. However, you will note that we can pass instances of both Person
and Job
types to keys
function which expects an argument of unamed type map[string]string
.
If you still find assignability of types confusing; I'd recommend you to read the explanations by Rob Pike in the following discussion.
Type Embedding
Previously, I mentioned when you declare a new type, it will not inherit the method set of the existing type. However, there's a way you can embed a method set of an existing type in a new type. This is possible by using the properties of annonymous field in a struct
type. When you define a annonymous field inside a struct
, all its fields and methods will be promoted to the defined struct type.
package main
type User struct {
Id int
Name string
}
type Employee struct {
User //annonymous field
Title string
Department string
}
func (u *User) SetName(name string) {
u.Name = name
}
func main(){
employee := new(Employee)
employee.SetName("Jack")
}
Here the fields and methods of User
type get promoted to Employee
, enabling us to call SetName
method on an instance of Employee
type.
Type Conversions
Basically, you can convert between a named typed and its underlying type. For example:
type Mystring string
var my_str Mystring = Mystring("awesome")
var str string = string(my_str)
There are few rules to keep in mind when it comes to type conversions. Apart from conversions involving string types, all other conversions will only modify the type but not the representation of the value.
You can convert a string to a slice of integers or bytes and vice-versa.
[]byte("hellø")
string([]byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'})
More robust and complex run-time type manupilations are possible in Golang using the Interfaces and Relection Package. We'll see more about them in a future post.