Safe Go Type Conversions: Comprehensive Guide

Younis Jad
Lyonas
Published in
7 min readJul 7, 2023

--

Unsafe Conversion Example

This starter guide provides an overview of Go’s type system, with a focus on type conversion. It explores the basics of type assertion, conversion, and unsafe type conversion and provides best practices for using them effectively. Type Conversion

Type conversion in Go involves changing the type of a value to a different type. There are two ways of converting types in Go: Implicit conversion and Explicit conversion.

Implicit Conversion

Implicit conversionImplicit conversion occurs when Go converts the type of a value automatically. For instance, when we assign an integer value to a float variable, Go will automatically convert it to a float type. Here is an example of implicit conversion:

package main

import "fmt"

func main() {
var a float32 = 5.6
var b float64 = float64(a)

fmt.Printf("a = %v, b = %v\n", a, b)
}

this code will result an unexpected panic therefore it’s unwise to use implicit conversions, continue reading to learn more on how to do typecasting and assertions to safely write convertible types code.

Explicit conversion

Explicit conversion occurs when we use the conversion syntax to convert a value to a different type. The conversion syntax in Go is similar to other programming languages, as it requires enclosing the value in parentheses and specifying the type in front of them. Here is an example of explicit conversion:

package main

import "fmt"

func main() {
var a float64 = 5.6
var b int = int(a)

fmt.Printf("a = %v, b = %v\n", a, b)
}

In the code above, the value of variable a is explicitly cast to an integer, and it is assigned to variable b. The int function is used to perform the type conversion, and the resulting value is truncated to an integer.

Fundamental type conversion in Go

Type conversion is a fundamental concept that plays a significant role in Go programming. It provides a powerful tool for conversion values to different types, thus allowing us to operate on the same data in different ways.

Converting an integer to a float:

var i int = 10
var f float64 = float64(i)

Converting a float to an integer:

var f float64 = 10.5
var i int = int(f)

Converting a string to a byte slice:

var s string = "hello world"
var b []byte = []byte(s)

Converting a byte slice to a string:

var b []byte = []byte{104, 101, 108, 108, 111}
var s string = string(b)

Converting an interface to a specific type:

var i interface{} = "hello world"
var s string = i.(string)

Converting a specific type to an interface:

var s string = "hello world"
var i interface{} = s

Safe Type Conversions using Type Assertions

Type assertion is a technique used in Go to extract the underlying value of an interface value and test whether it matches a specific type. There are two types of type assertion in Go: assertion with a single value and assertion with multiple values.

package main

import "fmt"

func f(i interface{}) {
switch i.(type) {
case int:
fmt.Println("This is an integer")
case string:
fmt.Println("This is a string")
default:
fmt.Println("I don't know what this is")
}
}

func main() {
f("hello")
f(42)
f(false)
}

In the above code, the f function takes an interface{} value and uses a switch statement to perform type assertion with multiple values. It tests the assertion of the value to either an int or a string type, and if the assertion is true, it prints a message. If the assertion fails, it moves to the default clause and prints a message indicating that it does not know what the value is.

here’s an example of using type assertion with implicit type conversion to capture panic and recover if any and provide a default value:

package main

import (
"fmt"
)

func main() {
var i int16 = 10
var i64 int64
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
i64 = 0
}
}()
i64 = int64(i) // Implicit type conversion with type assertion
fmt.Println("i64:", i64)
}

In the above code, we have an int16 variable i, and we want to convert it to an int64 variable i64. We use the type assertion syntax for an implicit type conversion since we know the conversion will be successful.

However, to demonstrate how to handle panics from incorrect type assertions, we add a defer statement that recovers from any panic that may occur during the execution of the program. Then, we use the int64 type with the variable i by using it within the type assertion, which auto-converts I safely into an int64 value.

If a panic occurs during the type assertion, the defer function is executed, where we print a message indicating that we recovered from the panic. We also set the value of i64 to a default value of 0 to avoid the program from crashing further.

Finally, we print the value of i64 whether it was set correctly or as a default value.

Output: i64: 10

So given that the type assertion is successful, the output will print the converted value of i to i64.

Unsafe Type Conversions

In some instances, it may be necessary to perform type conversion that is not safe. Go provides a package called unsafe that contains functions that perform unsafe type conversion.

The unsafe package provides a way for Go programmers to bypass the compile-time type checking and memory safety checks enforced by the language.

However, it is essential to use the unsafe package with caution as its use may lead to unexpected runtime behavior and memory corruption.

example of unsafe type conversion:

package main

import (
"fmt"
"unsafe"
)

func main() {
var i uint32 = 10
var ptr *uint32
ptr = (*uint32)(unsafe.Pointer(&i))
*ptr = 20
fmt.Printf("i = %d\n", i)
}

The use of unsafe type conversion is discouraged, and it should only be used when it is necessary, and when there are no other options available. It is important to understand the implications of using unsafe type conversion and to test it carefully to ensure the safety of the code.

Here’s an example of using type assertion with unsafe type conversion and defer from panic:

package main

import (
"fmt"
"unsafe"
)

type MyStruct struct {
x int
y string
}

func main() {
myStruct := MyStruct{x: 1, y: "hello"}

var ptr = unsafe.Pointer(&myStruct)
var ptrToInt = (*int)(ptr)
var i = *ptrToInt

fmt.Println("Value of i:", i)

defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()

var myString = myStruct.y
var ptrToString = (*string)(unsafe.Pointer(&myString))
var myIntString = *ptrToString
fmt.Println("My string:", myIntString)
}

In this example, we first create a basic struct MyStruct with an integer value x and a string value y. We then create a pointer called ptr and point it to our MyStruct.

After that, we create another pointer ptrToInt of int type and set it equal to ptr. We then dereference ptrToInt and save it to a variable i.

We then print the value of i, which should be the value of MyStruct.x.

Note: we use a defer function to handle any panics that occur during runtime.

We then declare a string variable myString and set it to MyStruct.y. We create another pointer called ptrToString, which is of string type and set it to the address of myString.

We then dereference ptrToString and save it to myIntString, which we then print to the console.

Note that in this example, we used unsafe pointers and type conversion. We should use them with caution, as they can lead to unexpected and incorrect behavior if not used correctly.

Type conversion and Conversion Best Practices

Here are a few best practices that can be followed when working with type conversion and assertion in Go:

Always check and handle errors when performing type assertion with multiple values. Here’s an example:

package main

import (
"fmt"
)

func main() {
var x interface{} = 42
if n, ok := x.(int); ok {
fmt.Printf("x holds an int value of %d\n", n)
} else if s, ok := x.(string); ok {
fmt.Printf("x holds a string value of %s\n", s)
} else {
panic("x does not hold an int or string value")
}
}

Avoid using unsafe type conversion and assertion as much as possible. Instead, use safe alternatives wherever available. Here’s an example:

package main

import (
"fmt"
"strconv"
)

func main() {
i := 10
s := strconv.Itoa(i)
fmt.Printf("The type of s is %T\n", s)
}

Use custom types to provide safety and clarity. Custom types can provide more safety and clarity in the code, and they can also reduce the number of type assertions required. Here’s an example:

package main

import (
"fmt"
)

type MyInt int

func (mi MyInt) IsPositive() bool {
return mi > 0
}

// usage
func main() {
var x MyInt = 42
if x.IsPositive() {
fmt.Println("The value of x is positive.")
}
}

Keep type conversion and assertion logic separate from the main logic of the program. This makes it easier to test and modify the code when necessary. Here’s an example:

package main

import (
"fmt"
"log"
"net/http"
"strconv"
)

func getIntValue(x interface{}) (int, error) {
if n, ok := x.(int); ok {
return n, nil
} else if s, ok := x.(string); ok {
i, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
return i, nil
} else {
return 0, fmt.Errorf("invalid type %T", x)
}
}

func main() {
resp, err := http.Get("http://example.com")
if err != nil {
log.Fatal(err)
}

contentLength, err := getIntValue(resp.Header.Get("Content-Length"))
if err != nil {
log.Fatal(err)
}

fmt.Printf("Content-Length: %d\n", contentLength)
}

By keeping the type assertion logic separate from the main logic, we can easily test and modify this function when necessary.

These best practices aim to ensure that the code is safe, readable, and maintainable. Always remember to consider these practices in order to write high-quality and robust Go code.

In the Next Article we will Discuss Type conversion with Struct and Interface Types for JSON

Thank You for Reading!

Leave a Comment and Follow for More Content Like This.

--

--

Younis Jad
Lyonas

Tech Evangelist, Experienced software engineer, Passionate about learning and building innovative, scalable solutions.