要理解类型转换(type conversion),那么首先要知道有哪些类型,请先阅读这篇文章 [go 数据系统]()。

知道了 go 中数据类型有哪些,我们来大概了解一下数据类型的转换有些什么不同,我们知道 go 是静态的强类型语言,也就是说声明变量的时候需要字母数据类型,虽然我们有 := 这种语法糖,但实际上编译器自动确定了类型。还有不同变量间需要手动进行转换。

虽然 go 是静态语言,但 go 中也有 interface 这样的动态类型,因此设计它的转换比较特殊,需要单独讲解。

基本数据类型转换

数字类型间的转换

数字类型包括 int 系和 float 系,它们间可以直接转换,只需要注意的是,高精度转换为低精度会丢失精度。

语法:varOfTypeB = typeB(varOfTypeA) ,即 B 类型变量 = 类型B( A 类型变量)

int 转 int32, int64

var a int = 10
// int 型
fmt.Printf("var a int = 10, a :%v\n", reflect.TypeOf(a))
// int 型转 int32
fmt.Printf("int32(a), a :%v\n", reflect.TypeOf(int32(a)))
// int 型转 int64
fmt.Printf("int64(a), a :%v\n", reflect.TypeOf(int64(a)))

int32, int64 转 int

var a int32 = 10
// int32 型
fmt.Printf("var a int32 = 10, a :%v\n", reflect.TypeOf(a))
var b int64 = 20
// int64 型 
fmt.Printf("var b int64 = 20, b :%v\n", reflect.TypeOf(b))
// int32 转 int, 容易精度丢失
fmt.Printf("int(a), a: %v\n", reflect.TypeOf(int(a)))
// int64 转 int, 容易精度丢失
fmt.Printf("int(b), b: %v\n", reflect.TypeOf(int(b)))

float 转 int

var a float32 = 10
// float32 型
fmt.Printf("var a float32 = 10, a : %v\n", reflect.TypeOf(a))
// float32 转 int
fmt.Printf("int(a), a : %v\n", reflect.TypeOf(int(a)))

数字类型和 string 间的相互转换

  • int, int32, int64 -> string

数字类型和 string 的底层类型不同,因此不能直接使用 T(X) 格式强制转换,需要借助 go 内置的一些函数

strconv 包就是用于 string 转换相关的,里面有两个函数比较常用 Atoi 和 Itoa,我们知道,计算机中一般使用 char array 存储 string, 所以 Atoi 就是 array to int 的缩写,也就是 string 转 int, 同理 Itoa 即 int to array ,即 int 转 string

我们来看 Itoa 的源码

func Itoa(i int) string {
	return FormatInt(int64(i), 10)
}

我们发现 Itoa 函数传入一个 int 型,返回 string 型,就是把 int 型转为 string 型,我们刚刚已经知道,int 型间可以直接转换的,所以如果数值不是很大的话,我们可以把 int32,int64 均转为 int,再用 Itoa 函数转换,自然 int8, in16 比 int 精度还小,没什么问题,

我们来把低精度的 int, in32, int64 转为 string

var a int = 10
fmt.Printf("var a int = 10, a := %v\n", reflect.TypeOf(a))
fmt.Printf("strconv.Itoa(a), a : %v\n", reflect.TypeOf(strconv.Itoa(a)))

var b int32 = 10
fmt.Printf("var b int32 = 20, b := %v\n", reflect.TypeOf(b))
fmt.Printf("strconv.Itoa(int(b)), b : %v\n", reflect.TypeOf(strconv.Itoa(int(b))))

var c int64 = 10
fmt.Printf("var c int64 = 30, c := %v\n", reflect.TypeOf(c))
fmt.Printf("strconv.Itoa(int(c)), c : %v\n", reflect.TypeOf(strconv.Itoa(int(c))))

当然,这种方法只适用于精度不大的情况,如果精度大的话,会有精度损失,我们如果细心一点,会发现 Itoa 的源码中,直接返回了FormatInt(int64(i), 10),我们发现它传入了一个 int64 型,显然低精度到高精度不会有精度损失,又是我们自然想到可能可以用这个函数,我们来看 FormatInt 的源码

func FormatInt(i int64, base int) string {
	if fastSmalls && 0 <= i && i < nSmalls && base == 10 {
		return small(int(i))
	}
	_, s := formatBits(nil, uint64(i), base, i < 0, false)
	return s
}

我们发现此函数传入了一个 int64 和 int,返回一个 stirng,也就是把 int 进制的一个 int64 数转为 string 型,显然这个函数更加精缺,刚刚的 Itoa 函数只是封装了一下,默认是 10 进制而已。

我们来看实际代码,把高精度的 int32, int64 转为 stirng

var b int32 = 123456789
fmt.Printf("var b int32 = 123456789, b := %v\n", reflect.TypeOf(b))
fmt.Printf("reflect.TypeOf(strconv.FormatInt(int64(b), 10), b : %v\n",reflect.TypeOf(strconv.FormatInt(int64(b), 10)))

var c int64 = 123456789
fmt.Printf("var c int64 = 123456789, c := %v\n", reflect.TypeOf(c))
fmt.Printf("reflect.TypeOf(strconv.FormatInt(int64(c), 10), c : %v\n",reflect.TypeOf((strconv.FormatInt(int64(c), 10))))

当然还有其他的,如无符号型转 string,float 转 string, 等,这些可以继续看 strconv.FormatUint() 和 strconv.FormatFloat() 等函数,由又不常用,这里就不展开了。

  • string -> int, int32, int64

我们来继续看 Atoi 的源码

func Atoi(s string) (int, error) {
   //
   	i64, err := ParseInt(s, 10, 0)
	if nerr, ok := err.(*NumError); ok {
		nerr.Func = fnAtoi
	}   
	return int(i64), err
}

我们发现 Atoi 传入一个 stirng, 返回一个 int 和 error,同 Itoa 函数,这里我们同样发现精度问题,如果精度比较小,那么转为 int 型显然没什么问题,那这就变成 string 转为 int,之后再把 int 转为 int32 或 int64, 我们实际运用一下

str := "1234"
i, _ := strconv.Atoi(str)
fmt.Println(reflect.TypeOf(i))
fmt.Println(reflect.TypeOf(int32(i)))
fmt.Println(reflect.TypeOf(int64(i)))

同样的,如果我们遇到精度高的情况,这种情况是不行的,我们来继续观察 Atoi 的源码,我们发现最后 return 时,返回的是 int(i64), 这显然是把 int64 转为了 int 型,这里就降低了精度,而 i64 又是由 ParseInt 函数而来,显然 Atoi 又是一个被封装好的函数,为了保持精度,我们来看 ParseInt 的源码

func ParseInt(s string, base int, bitSize int) (i int64, err error) {
  //
  return n, nil
}

我们发现传入了一个 string, 两个 int, 返回一个 int64 和 eror, 仔细观察,显然两个 int ,一个是 进制,一个是比特数,那么我们要把 string 转为 int32 或 int64 又不损失精度,就比较方便了,我们来看实例

// string 转换成int64
strInt64, _ := strconv.ParseInt(str, 10, 64)

// string转换成int32
strInt32, _ := int32(strconv.ParseInt(str, 10, 32))

  • []byte 和 string

上面我们讲解了 string 和 int,int32,int64 间的相互转换,实际上还有一种常见的转换,我们知道 byte 是 uint8 的别名, uint8

就是无符号 8 比特,也就是 一字节,所以 go 设置了个别名 byte, 很多地方都会用到字节流,在 go 中也就是 []byte, 所以 []byte 和 string 间的相互转换也很常见。

string 转 字节切片([]byte)

str := "hahah"
bs := []byte(str)

字节切片([]byte) 转 string

bs := []byte("asf")
str := string(bs)

自定义类型

我们刚讲了 go 自带的底层类型,现在我们来讲一下是 Type 关键字声明的自定义类型,我们知道 type 声明的类型最终都是由一些底层类型组合而来,那么这些类型间的相互转换又有什么关系呢,我们来看代码

type myInt int

var a myInt = 10

b := int(a)

fmt.Println(reflect.TypeOf(b))

我们声明了一个以 int 为底层类型的 myInt 类型,然后实例一个 a, 再把 a 转换为 int 类型,发现并没有报错,我们继续

type myInts myInt
type myInt  int

var a myInt = 10

b := int(a)
fmt.println(reflect.TypeOf(b))

我们再自定义类型上又嵌套了一个自定义类型,仍然没有报错,这似乎说明底层类型相同就是转换,那我们来弄个底层类型不同的试试

type dog struct {
	age  int
	name string
}
type cat struct {
	name string
}

d := dog{}

fmt.Println(reflect.TypeOf(cat(d)))

会报 error cannot convert d (type dog) to type cat,那我们给 cat 加上 age filed 试试

type dog struct {
	age  int
	name string
}
type cat struct {
	age  int
	name string
}

d := dog{}

fmt.Println(reflect.TypeOf(cat(d)))

结果成功了,那我们可以得出结论了,对于自定义类型,只要底层类型相同,就可以使用varOfTypeB = typeB(varOfTypeA) 语法强制转换。

接口转换

我们知道接口 interface 动态类型的,那么它的类型就是不固定的,要转换它的类型,就需要判断它的类型,所以这里需要用到断言,断言也就是一段逻辑表达式,在这里就是类型断言。如果对接口不了解,请先阅读这篇文章 [接口](),如果对断言不了解,请先阅读这篇文章 [断言]()

接口转换为其他类型

类型断言涉及到接口,我们先给出类型断言的语法

varOfTypeA = varOfInterfaceB.(TypeA)

A类型变量 = 接口变量 B.(类型 A)

例子

// 建一个空接口 A
type A interface{}
// 实例一个 A 变量 a,并赋值为 int 型
var a A = 123
// 判断 a 是否为 string,并输出结果 Type
fmt.Println(reflect.TypeOf(a.(string)))

我们得到了一个 pannic interface conversion: main.A is int, not string,显然失败了,我们再改为 a.(int) 试试,得到结果 int, 显然成功了。这就是最基本的类型断言,把接口变量转换为其他类型的变量。但是刚刚的例子我们也发现我们难以判断是否类型断言成功了,不够好在 go 还给我们提供了一种语法,请看

varOfTypeA,ok  = varOfInterfaceB.(TypeA)

我们发现多了一个返回值,是的,这个返回值是一个 bool 类型,用于判断是否断言成功,我们一般用 ok 表示,我们看例子

// 建一个空接口 A
type A interface{}
// 实例一个 A 变量 a,并赋值为 int 型
var a A = 123
// 判断 a 是否为 string,并输出结果 Type
//fmt.Println(reflect.TypeOf(a.(string)))

// 这里自行把 string 改为 int,看是否改变了分支
_, ok := a.(string)
if !ok {
	fmt.Println(ok)
	fmt.Println("断言失败!")
} else {
	fmt.Println(ok)
	fmt.Println("断言成功!")
}

从上面我们这个 ok 断言我们发现非常方便,其他还有类型的用法,如判断 map 中 key 是否存在的断言,判断 channel 中是否有值的断言等,具体的请查看 [断言]() 这篇文章。

其他类型转换为接口

其他类型转换为接口 interface ,这是我们日常中比较常见的用法,我们知道 interface 是方法的集合,其他类型只要方法集是接口方法集的超集,那么这个类型就自动的实现了这个接口,那么这个类型就可以直接转换为接口。具体请查看 [接口]()这篇文章

看例子

// Animal 接口
type Animal interface {
	Speak(msg string)
}

type dog struct {
}

// dog 实现了 Animal 接口
func (d dog) Speak(msg string) {
	fmt.Println("dog say:" + msg)
}

type cat struct {
}

// cat 实现了 Animal 接口
func (c cat) Speak(msg string) {
	fmt.Println("cat say:" + msg)
}

func main() {
	var animal Animal
	// animal 接口转为 dog struct
	animal = dog{}
	animal.Speak("i'm dog")
	// animal 接口转为 cat struct
	animal = cat{}
	animal.Speak("i'm cat")
}