Conversions in Go

Michał Łowicki
golangspec
6 min readOct 24, 2016

--

There are times where it might be desirable to convert value to other type. Golang doesn’t allow to do it in arbitrary way. They’re certain rules enforced by the type system. In this story we’ll see what kind of conversions are possible, which ones aren’t and when conversions are actually useful.

Go is strongly typed programming language. It’s strict when it goes to types and mistakes will be reported during compilation:

package mainimport "fmt"func main() {
monster := 1 + "2"
fmt.Printf("monster: %v\n", monster)
}
> go build
# github.com/mlowicki/lab
./lab.go:6: cannot convert "2" to type int
./lab.go:6: invalid operation: 1 + "2" (mismatched types int and string)

JavaScript is one of weakly (loosely) typed languages. Let’s see it in action:

var monster = 1 + "foo" + function() {};
console.info("type:", typeof monster)
console.info("value:", monster);

It might seem strange to add together number, string and even a function. No worries, JavaScript will do it for you without any complains:

type: string
value: 1foofunction () {}

Under certain circumstances there might be a need to convert value to other type f.ex. to pass it as an argument or maybe put it into an expression:

func f(text string) {
fmt.Println(text)
}
func main() {
f(string(65)) // integer constant converted to string
}

Expression from function call is an example of conversion and we’ll see more of them below. Foregoing code works just fine but removing conversion:

f(65)

causes compile-time error: “cannot use 65 (type int) as type string in argument to f”.

Underlying type

Underlying type of string, boolean, numeric or literal type is the same type. Otherwise it’s the underlying type from type declaration:

type A string             // string
type B A // string
type C map[string]float64 // map[string]float64 (type literal)
type D C // map[string]float64
type E *D // *D

(underlying types are placed inside comments)

If underlying types are the same then conversion is 100% valid:

package maintype A string
type B A
type C map[string]float64
type C2 map[string]float64
type D C
func main() {
var a A = "a"
var b B = "b"
c := make(C)
c2 := make(C2)
d := make(D)
a = A(b)
b = B(a)
c = C(c2)
c = C(d)
var _ map[string]float64 = map[string]float64(c)
}

Nothing blocks from compiling such program. Definition of underlying types isn’t recursive:

type S string
type T map[S]float64
...var _ map[string]float64 = make(T)

as it gives compilation-time error:

cannot use make(T) (type T) as type map[string]float64 in assignment

It happens because underlying type of T isn’t map[string]float64 but map[S]float64. Conversion won’t work neither:

var _ map[string]float64 = (map[string]float64)(make(T))

It triggers error while building:

cannot convert make(T) (type T) to type map[string]float64

Assignability

Go specifications coins the term assignability. It defines when value v is assignable to variable of type T. Let’s see its one rule in action, which says about the same underlying types when at least one type is not named:

package mainimport "fmt"func f(n [2]int) {
fmt.Println(n)
}
type T [2]intfunc main() {
var v T
f(v)
}

This program prints “[0 0]”. Conversion is possible in all cases allowed by assignability law. This way programmer can explicitly mark his / her conscious decision:

f([2]int(v))

Using above call gives the same result as before. More on assignability in previous post.

The first rule of conversion (the same underlying type) overlaps with one of the assignability rules — when underlying types are the same and at least one type is unnamed (first example in this section). Weaker rule shadows the one which is more strict. So finally while conversion only underlying types need to be the same — being named / unnamed type doesn’t matter.

Constants

Constant v can be converted to type T when v is representable by value of type T:

a := uint32(1<<32 – 1)
//b := uint32(1 << 32) // constant 4294967296 overflows uint32
c := float32(3.4e38)
//d := float32(3.4e39) // constant 3.4e+39 overflows float32
e := string("foo")
//f := uint32(1.1) // constant 1.1 truncated to integer
g := bool(true)
//h := bool(1) // convert 1 (type int) to type bool
i := rune('ł')
j := complex128(0.0 + 1.0i)
k := string(65)

In-depth introduction to constants is available on official blog.

Numeric types

floating-point number → integer

var n float64 = 1.1
var m int64 = int64(n)
fmt.Println(m)

Fractional part is removed so the code prints “1”.

For other conversions:

  • floating-point number → floating-point number,
  • integer → integer,
  • integer → floating-point number,
  • complex → complex.

value is rounded to destination precision:

var a int64 = 2 << 60
var b int32 = int32(a)
fmt.Println(a) // 2305843009213693952
fmt.Println(b) // 0
a = 2 << 30
b = int32(a)
fmt.Println(a) // 2147483648
fmt.Println(b) // -2147483648
b = 2 << 10
a = int64(b)
fmt.Println(a) // 2048
fmt.Println(b) // 2048

Pointers

Assignability deals with pointer types in the same way as with other type literals:

package mainimport "fmt"type T *int64func main() {
var n int64 = 1
var m int64 = 2
var p T = &n
var q *int64 = &m
p = q
fmt.Println(*p)
}

Program works fine and prints “2” relying on already covered assignability rule. Underlying types of *int64 and T are the same and additionally *int64 is unnamed. Conversion adds more options. For unnamed pointer types it’s enough that base types of pointers have the same underlying type:

package mainimport "fmt"type T int64
type U W
type W int64
func main() {
var n T = 1
var m U = 2
var p *T = &n
var q *U = &m
p = (*T)(q)
fmt.Println(*p)
}

*T is parenthesized as otherwise it would interpreted as *(T(q)).

Similarly as before program prints “2”. Underlying types of U and T are the same and they’re used as base types in *U and *T. Assignment:

p = q

won’t work because its deals with two different underlying types: *T and *U. As an exercise let’s what happens when we change declarations a bit:

type T *int64
type U *W
type W int64
func main() {
var n int64 = 1
var m W = 2
var p T = &n
var q U = &m
p = T(q)
fmt.Println(*p)
}

Declarations of U and W have been changed. Think for a moment what’ll happen…

The compiler points out an error “cannot convert q (type U) to type T” at:

p = T(q)

It’s because the underlying type of p’s type is *int64 but for q’s type it’s *W. Type of q is named (U) so rule about getting underlying type of pointer base type doesn’t apply here.

Strings

integer → string

Passing number N to string built-in converts it to UTF-8 encoded string containing symbol represented by N.

fmt.Printf("%s\n", string(65))
fmt.Printf("%s\n", string(322))
fmt.Printf("%s\n", string(123456))
fmt.Printf("%s\n", string(-1))

output:

A
ł

Two first conversions use completely valid code points. You may wonder what is the strange symbol displayed in last two lines. It’s a replacement character which is a member of Unicode block called specials. Its code is \uFFFD (more info).

Short introduction to strings

Strings are basically slices of bytes:

text := "abł"
for i := 0; i < len(text); i++ {
fmt.Println(text[i])
}

output:

97
98
197
130

97 and 98 are UTF-8 encoded “a” and “b” characters. 3rd and 4th lines present encoding of letter “ł” which in UTF-8 takes two bytes.

Range loop helps to iterate through code points as defined by Unicode (code point in Go is called rune):

text := "abł"
for _, s := range text {
fmt.Printf("%q %#v\n", s, s)
}

output:

'a' 97
'b' 98
'ł' 322

To learn more about format verbs like %q or %#v see documentation of fmt package.

Much more information in “Strings, bytes, runes and characters in Go”. Conversion between strings and slice of bytes or runes shouldn’t be that strange anymore after this quick explanation.

string ↔ slice of bytes

bytes := []byte("abł")
text := string(bytes)
fmt.Printf("%#v\n", bytes) // []byte{0x61, 0x62, 0xc5, 0x82}
fmt.Printf("%#v\n", text) // "abł"

Slice contains bytes of UTF-8 converted string.

string ↔ slice of runes

runes := []rune("abł")
fmt.Printf("%#v\n", runes) // []int32{97, 98, 322}
fmt.Printf("%+q\n", runes) // ['a' 'b' '\u0142']
fmt.Printf("%#v\n", string(runes)) // "abł"

Slice created from converted string contains Unicode code points (runes).

--

--

Michał Łowicki
golangspec

Software engineer at Datadog, previously at Facebook and Opera, never satisfied.