Skip to content

Types

In the previous section on data types, all built-in data types in Go have been briefly introduced. These built-in basic types form the foundation for subsequent custom types. Go is a typical statically typed language, where the type of all variables is determined at compile time and does not change throughout the life of the program. This section will briefly introduce Go's type system and basic usage.

Static Strong Typing

Go is a static strong typing language. "Static" means that the types of all Go variables are determined at compile time and do not change throughout the program's lifecycle. Although Go's short variable declaration syntax resembles dynamic language syntax, the variable type is inferred by the compiler. The fundamental difference is that once the type is inferred, it does not change. Dynamic languages work completely differently. Therefore, the following code cannot be compiled because a is an int type variable and cannot be assigned a string value.

go
func main() {
  var a int = 64
  a = "64"
  fmt.Println(a) // cannot use "64" (untyped string constant) as int value in assignment
}

"Strong typing" means that strict type checking is performed in the program. If a type mismatch occurs, the programmer is immediately informed that this should not be done, rather than attempting to infer a possible result like a dynamic language. Therefore, the following code cannot be compiled because the two types are different and cannot be operated on.

go
func main() {
  fmt.Println(1 + "1") // invalid operation: 1 + "1" (mismatched types untyped int and untyped string)
}

Type Suffix

Why does Go put the type declaration at the end rather than the beginning? To a large extent, it learned from C. Taking an official example to show the effect, this is a function pointer:

c
int (*(*fp)(int (*)(int, int), int))(int, int)

Honestly, it's hard to tell what type this is without careful examination. In Go, a similar declaration looks like this:

go
f func(func(int,int) int, int) func(int, int) int

Go's declaration style always follows the principle of name first, type second. Reading from left to right, at first glance you can tell this is a function, and the return value is func(int,int) int. When types become more complex, type suffix is much better in terms of readability. Many design aspects of Go serve readability.

Type Declaration

In Go, through type declarations, you can declare a new type with a custom name. Declaring a new type usually requires a type name and a basic type. A simple example is as follows:

go
type MyInt int64

In the above type declaration, a type named MyInt with a basic type of int64 is declared using the type keyword. In Go, every newly declared type must have a corresponding basic type, and it is not recommended for the type name to duplicate existing built-in identifiers.

go
type MyInt int64

type MyFloat64 float64

type MyMap map[string]int

// Can compile, but not recommended as it overrides the original type
type int int64

Types declared through type declarations are all new types, and different types cannot be operated on, even if the basic types are the same.

go
type MyFloat64 float64

var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(f1 + f)
go
invalid operation: f1 + f (mismatched types MyFloat64 and float64)

Type Aliases

Type aliases are different from type declarations. A type alias is just an alias and does not create a new type. A simple example is as follows:

go
type Int = int

Both are the same type, just with different names, so they can be operated on. Therefore, the following example can naturally be compiled.

go
type Int = int
var a Int = 1
var b int = 2
fmt.Println(a + b)
3

Type aliases are very useful for particularly complex types. For example, there is a type map[string]map[string]int, which is a two-dimensional map. Now there is a function parameter of type map[string]map[string]int, as follows:

go
func PrintMyMap(mymap map[string]map[string]int) {
   fmt.Println(mymap)
}

In this case, there is no need to use type declaration, because the former declares a new type that cannot be used as a parameter to this function. The example after using a type alias is as follows:

go
type TwoDMap = map[string]map[string]int

func PrintMyMap(mymap TwoDMap) {
   fmt.Println(mymap)
}

Using a type alias appears more concise.

TIP

The built-in type any is a type alias for interface{}. They are completely equivalent, just with different names.

Type Conversion

In Go, only explicit type conversion exists; implicit type conversion does not exist. Therefore, variables of different types cannot be operated on or passed as parameters. Type conversion applies under the premise that the type of the variable being converted and the target type to convert to are known. An example is as follows:

go
type MyFloat64 float64

var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(float64(f1) + f)
0.30000000000000004

Addition can only be performed by explicitly converting MyFloat64 to float64 type. Another prerequisite for type conversion is: the type being converted must be representable by the target type (Representability). For example, int can be represented by int64 type and can also be represented by float64 type, so explicit type conversion can be performed between them. However, int type cannot be represented by string and bool types, so type conversion cannot be performed.

TIP

For the definition of Representability, you can refer to Reference Manual - Representability for more details.

Even if two types can represent each other, the result of type conversion is not always correct. Consider the following example:

go
var num1 int8 = 1
var num2 int32 = 512
fmt.Println(int32(num1), int8(num2))
1 0

num1 is correctly converted to int32 type, but num2 is not. This is a typical numeric overflow problem. int32 can represent 31-bit integers, while int8 can only represent 7-bit integers. When converting from high-precision integers to low-precision integers, high-order bits are discarded and low-order bits are retained. Therefore, the conversion result of num2 is 0. In numeric type conversion, it is usually recommended to convert from small to large, and not recommended to convert from large to small.

When using type conversion, ambiguity needs to be avoided for certain types. Examples are as follows:

go
*Point(p) // equivalent to *(Point(p))
(*Point)(p)  // convert p to type *Point
<-chan int(c)    // equivalent to <-(chan int(c))
(<-chan int)(c)  // convert c to type <-chan int
(func())(x)      // convert x to type func()
(func() int)(x)  // convert x to type func() int

Type Assertion

Type assertion is usually used to determine whether a variable of an interface type belongs to a certain type. An example is as follows:

go
var b int = 1
var a interface{} = b
if intVal, ok := a.(int); ok {
   fmt.Println(intVal)
} else {
   fmt.Println("error type")
}
1

Since interface{} is an empty interface type, and empty interface type can represent all types, but int type cannot represent interface{} type, type conversion cannot be used. Type assertion can determine whether the underlying type is the desired type. A type assertion statement has two return values: one is the value after type conversion, and the other is a boolean value indicating the conversion result.

Type Switch

In Go, the switch statement also supports a special syntax. Through this syntax, different logic processing can be performed based on different case conditions. The prerequisite for use is that the input parameter must be an interface type. An example is as follows:

go
var a interface{} = 2
switch a.(type) {
    case int: fmt.Println("int")
    case float64: fmt.Println("float")
    case string: fmt.Println("string")
}
int

TIP

Through the operations provided by the unsafe package, Go's type system can be bypassed, allowing type conversion operations that would otherwise fail to compile.

Golang by www.golangdev.cn edit