In Go, type conversion and type assertion are two important concepts that allow you to work with different types in a flexible manner. Let’s take a quick look at how they work and their limitations.
Basic Type Conversion
Heads up: constants are checked at compile time, variables are checked at runtime.
Constants conversion
In Go, constants are checked at compile time, so unsafe conversions are rejected. Here are some failed examples and please note the error messages in the comments.
const a = 3.14
fmt.Println(int(a)) // cannot convert a (untyped float constant 3.14) to type int
fmt.Println(int(3.14)) // cannot convert 3.14 (untyped float constant) to type int
const a float32 = 3.14
fmt.Println(int(a)) // cannot convert a (constant 3.14 of type float32) to type int
fmt.Println(uint8(300)) // constant 300 overflows uint8
As you can see, if there is truncation or overflow, the compiler will reject it. So it means if there is no truncation or overflow, we can safely convert between constants.
const a = 3
fmt.Println(int64(a)) // prints 3, safe conversion
const b = 255
fmt.Println(uint8(b)) // prints 255, safe conversion
Numbers To Numbers
Go require explicit conversion between different numeric types. In this process, it might slightly truncate or overflow, you should make sure the conversion is safe by yourself.
Float to Int truncates the decimal part, does not round.
var f floag = 3.14
var i int = int(f) // Explicit conversion from float to int, it prints 3.
Larger Int to Smaller Int might overflow.
var n int = 300
var b uint8 = uint8(n) // Explicit conversion from int to uint8, it prints 44 due to overflow.
Negative int to uint reinterprets bits
var n int = -42
var u uint = uint(n) // it prints 18446744073709551574 on a 64-bit system, which is 2^64 - 42.
Strings <-> Slices
There are three cases:
- String to []byte: converts each character to its UTF-8 byte value.
- String to []rune: converts each character to its Unicode code point.
- []byte or []rune to String: converts the byte or rune slice to a string.
Boolean conversions
Go does not allow implicit conversion from number to bool, which means 0 does not mean false, and non-zero does not
mean true.
We must explicitly compare the value.
var x int = 1
if x != 0 { ... }
Defined types and Underlying types
We can create custom types based on existing types. Even if the underlying type is the same, Go treats them as distinct So we need to explicitly convert between them.
type MyInt int
var a Myint = 42
var b int = a //cannot use a (variable of int type MyInt) as int value in variable declaration
var c int = MyInt(b) // Explicit conversion from MyInt to
type Celsius float64
type Fahrenheit float64
var c Celsius = 100
var f Fahrenheit = Fahrenheit(c)
Interfaces and Type Assertion
interface{}can hold any value. And Type Assertion are used for interfaces.
var x interface{}
x = 42
x = "hello"
- Type Assertion extracts the concrete type:
var a interface{} = "hello"
s := a.(string) // s is "hello"
Safe assertionusing comma-ok. Without ok, wrong type assertion panics at runtime. With ok, returns *zero value + false if types don’t match.
var a interface{} = 42
s, ok := a.(string) // ok is false, s is zero value ""
Interface -> Interface
If underlying type satisfies target interface, conversion succeeds.
type Writer interface {
Write([]byte) (int, error)
}
type Reader interface {
Read([]byte) (int, error)
}
type ReadWriter interface {
Reader
Writer
}
type MyFile struct {
name string
}
func (f *MyFile) Read([]byte) (int, error) { return 0, nil }
func (f *MyFile) Write([]byte) (int, error) { return 0, nil }
func main() {
var f *MyFile = &MyFile{"test.txt"}
var w Writer = f
var r Reader = f
var rw ReadWriter = f
fmt.Printf("w: %T\n", w) // *main.MyFile
fmt.Printf("r: %T\n", r) // *main.MyFile
fmt.Printf("rw: %T\n", rw) // *main.MyFile
var w2 Writer = rw // ReadWriter -> Writer
fmt.Printf("w2: %T\n", w2)
if rw2, ok := w.(ReadWriter); ok {
fmt.Printf("rw2: %T\n", rw2)
}
}
Pointer Conversions
Pointers are types, converting unrelated pointer types directly is disallowed, we need to involve unsafe.Poninter as a
bridge. so it’s like this *T1 -> unsafe.Pointer -> *T2.
var p *int
var i *int64 = p // cannot use p (variable of type *int) as *int64 value in variable declaration
var p *int
var i *int64 = (*int64)(unsafe.Pointer(p))