文章目录
- 类型转换
- 1. 数值类型之间的转换
- 2. 字符串和字节切片
- 3. 字符串和数字之间的转换(最常用)
- 4. 字符串和 bool 的转换
- 5. 接口类型转换
- 5.1 接口类型断言
- 5.2 接口 -> 接口转换
- 6. 切片类型转换的问题
- 7. 指针转换
- 8. 小结
本系列文章:
- Java 转 go 学习 - 项目管理
- Java 转 go 学习 - 基本语法
类型转换
1. 数值类型之间的转换
| 转换方向 | 说明 | 风险 |
|---|---|---|
int→int64 | 小转大 | ✅ 安全 |
int64→int | 大转小 | ⚠️ 可能溢出 |
int→float64 | 整数转浮点 | ✅ 安全 |
float64→int | 浮点转整数 | ⚠️ 丢失小数部分 |
int→byte | 转8位 | ⚠️ 可能溢出 |
例子如下:
packagemainimport"fmt"funcmain(){// 语法:目标类型(值)variint=10varffloat64=float64(i)// int → float64varbbyte=byte(i)// int → bytevari64int64=int64(i)// int → int64fmt.Println(f,b,i64)// 10 10 10// 精度丢失varbigint64=1000varsmallint8=int8(big)fmt.Println(big,small)// 1000 -24// 浮点数转整数, 小数丢失varfloatfloat64=10.5fmt.Println(int(float))// 10}2. 字符串和字节切片
其实就是 string 和 byte[] 的转换,类比 Java 的 String 和 byte[] 转换。
functest2(){// 字符串 → []bytes:="hello"b:=[]byte(s)// []byte → 字符串b2:=[]byte{104,101,108,108,111}s2:=string(b2)// 字符串 → []rune (处理 Unicode)s3:="你好"runes:=[]rune(s3)fmt.Println(b,s2,runes)// [104 101 108 108 111] hello [20320 22909]}3. 字符串和数字之间的转换(最常用)
下面用 ai 帮我整理了个表格,比较直观看到类型的转换函数,看到这些函数又想起了当年学 c 的日子:
| 转换方向 | 目标类型 | 推荐函数 (高效/标准) | 备选函数 (通用/格式化) | 说明与注意事项 |
|---|---|---|---|---|
| 任意 → 字符串 | ||||
bool→string | string | strconv.FormatBool(b) | fmt.Sprintf("%v", b) | FormatBool返回 “true” 或 “false”。 |
int→string | string | strconv.Itoa(i) | fmt.Sprintf("%d", i) | Itoa是FormatInt(i, 10)的简便写法,性能更好。 |
int64→string | string | strconv.FormatInt(i, base) | fmt.Sprintf("%d", i) | base通常为 10。支持 2~36 进制转换。 |
uint→string | string | strconv.FormatUint(i, base) | fmt.Sprintf("%d", i) | 用于无符号整数。 |
float→string | string | strconv.FormatFloat(f, 'f', -1, 64) | fmt.Sprintf("%g", f) | 'f': 定点数;-1: 自动精度;64: float64。可用'g'自动去零。 |
| 字符串 → 任意 | ||||
string→bool | bool | strconv.ParseBool(s) | - | 仅接受:"1", "t", "T", "true", "TRUE", "True"(及其 false 对应值)。 |
string→int | int | strconv.Atoi(s) | fmt.Sscanf(s, "%d", &i) | Atoi是ParseInt(s, 10, 0)的简便写法,返回int。最常用。 |
string→int64 | int64 | strconv.ParseInt(s, base, bitSize) | - | base: 进制(10);bitSize: 位数(0=int, 64=int64)。 |
string→uint | uint | strconv.ParseUint(s, base, bitSize) | - | 解析无符号整数。 |
string→float | float64 | strconv.ParseFloat(s, bitSize) | fmt.Sscanf(s, "%f", &f) | bitSize通常为 64。若需 float32 则传 32。 |
| 其他实用转换 | ||||
[]byte↔string | 互转 | string(b)/[]byte(s) | - | 零拷贝 (逻辑上),Go 特有语法,无需 strconv。 |
int→[]byte | []byte | strconv.AppendInt([]byte{}, i, 10) | - | 避免中间 string 分配,直接追加到字节切片,高性能场景推荐。 |
下面简单看下一些例子:
functest4(){// --- 数字转字符串 ---n:=123// 一般都是用这个函数, 转换最快, int -> strings1:=strconv.Itoa(n)fmt.Println(s1,n)// 123 123// 支持格式化 (如加前缀、补零)s2:=fmt.Sprintf("%05d",n)fmt.Println(s2)// 00123// int64 或指定进制 (如二进制), 第二个参数就是指定进制s3:=strconv.FormatInt(1000,2)s4:=strconv.FormatInt(1000,8)s5:=strconv.FormatInt(1000,16)fmt.Println(s3,s4,s5)// 1111101000 1750 3e8// --- 字符串转数字 ---str:="456"// 专用于 string -> int, 一般都是用这个val1,err1:=strconv.Atoi(str)iferr1==nil{fmt.Println(val1)// 456}// 需要处理 int64 或特定进制时使用, 第二个参数就是指定进制数val2,err2:=strconv.ParseInt(s3,2,64)fmt.Println(err2,val2)// <nil> 1000val3,err2:=strconv.ParseInt(s4,8,64)fmt.Println(err2,val3)// <nil> 1000val4,err2:=strconv.ParseInt(s5,16,64)fmt.Println(err2,val4)// <nil> 1000// 浮点数fStr:="3.14159"fVal,_:=strconv.ParseFloat(fStr,64)fmt.Println(fVal)// 3.14159// Parse 系列函数不会自动去除空格atoi,err:=strconv.Atoi(" 123 ")println(err.Error(),atoi)// strconv.Atoi: parsing " 123 ": invalid syntax 0// 会报错,需先通过 TrimSpace 清空atoi1,err1:=strconv.Atoi(strings.TrimSpace(" 123 "))println(err1,atoi1)// (0x0,0x0) 123}⚠️ 在循环中大量拼接数字时,直接使用 Append 系列函数可以减少内存分配(GC 压力):
// 不推荐:每次循环都分配新的 stringfori:=0;i<1000;i++{s:=strconv.Itoa(i)// ...}// 推荐直接操作 []bytebuf:=make([]byte,0,64)fori:=0;i<1000;i++{buf=strconv.AppendInt(buf,int64(i),10)buf=append(buf,',')}result:=string(buf)// 最后再转一次 string最后有一个注意点:
fmt.Println(string(65))这行代码最终会输出 A,而不是 65,所以将 int 转 string,需要用strconv.Itoa或fmt.Sprintf。
4. 字符串和 bool 的转换
ParseBool 可以将字符串转成 bool,支持:"1", "t", "T", "true", "TRUE", "True"等字符串,下面是示例:
functest5(){b:=true// bool -> strings:=strconv.FormatBool(b)fmt.Println(s)// true// string -> bool// 支持: "1", "t", "T", "true", "TRUE", "True" 等b2,err:=strconv.ParseBool("True")b3,err:=strconv.ParseBool("1")b4,err:=strconv.ParseBool("t")b5,err:=strconv.ParseBool("T")b6,err:=strconv.ParseBool("true")b7,err:=strconv.ParseBool("True")fmt.Println(b7,b2,b3,b4,b5,b6,err)// true true true true true true <nil>}在循环中大量拼接数字时,直接使用 Append 系列函数可以减少内存分配(GC 压力):
5. 接口类型转换
5.1 接口类型断言
假设现在有一个空接口,上一篇文章就说明了空接口其实就相当于 Java 的 Object 类,可以存储任何类型的数据,类型断言就是说:我知道这个接口变量里实际存的是某种具体类型,我要把它‘还原’成那个具体类型,以便使用该类型特有的方法或属性。
🔹 语法如下:
value,ok:=interfaceVariable.(ConcreteType)- interfaceVariable:接口类型的变量,比如
var i interface{} = "hello"。 - ConcreteType:接口具体类型,比如上面的
string。 - value:断言成功后,转换后的具体类型的值。
- ok:bool 类型,是否断言成功,如果断言失败就是这个类型的零值,比如
string的空字符串,int的 0。
下面有两种方式去断言,一种是判断 ok,一种就是不管 ok:
| 用法 | 代码示例 | 行为 | 推荐场景 |
|---|---|---|---|
| 安全断言 (双值) | s, ok := i.(string) | 如果类型不对,ok为false,程序继续运行。 | 绝大多数场景,特别是处理外部输入、JSON解析、不确定类型时。 |
| 直接断言 (单值) | s := i.(string) | 如果类型不对,程序直接 Panic (崩溃)。 | 你100% 确定类型正确,或者希望类型错误时直接让程序崩溃以暴露 Bug。 |
下面举个例子,首先是安全断言:
functest6(){variinterface{}="hello"// 1. 断言为 strings,ok:=i.(string)ifok{fmt.Println(s,len(s))// hello 5}// 2. 尝试断言为 int (实际上里面是 string)n,ok:=i.(int)if!ok{fmt.Println("断言失败:这确实不是一个整数",n)// 断言失败:这确实不是一个整数 0// n 的值现在是 0 (int 的零值),但程序没挂}else{fmt.Println(n)}}可以看到通过 ok 判断是否断言不会程序异常,下面是直接断言,这种就比较危险,断言失败就会程序退出。
functest7(){variinterface{}="hello"// 如果里面真的是 string,没问题s:=i.(string)fmt.Println(s)// 如果里面不是 int,下面这行会让程序直接崩溃 (panic)n:=i.(int)// panic: interface conversion: interface {} is string, not intfmt.Println(n)}当然如果不知道接口是什么类型,可以通过 switch 去判断:
funcdescribe(iinterface{}){// v 是根据 case 自动转换后的具体类型的值switchv:=i.(type){caseint:// 在这里 v 是 int 类型fmt.Printf("整数: %d\n",v*2)casestring:// 在这里 v 是 string 类型fmt.Printf("字符串: %q\n",v)casebool:fmt.Printf("布尔: %v\n",v)default:// 未知类型,v 保持 interface{} 类型fmt.Printf("未知类型: %T\n",v)}}5.2 接口 -> 接口转换
这种接口到接口的转换就是隐式类型转换或接口赋值,比如下面的例子:
functest8(){typeReaderinterface{Read(p[]byte)(nint,errerror)}typeWriterinterface{Write(p[]byte)(nint,errerror)}// ReadWriter 嵌入了 Reader 和 Writer,所以它必须同时实现 Read 和 WritetypeReadWriterinterface{Reader;Writer}// 这是一个大接口变量varrw ReadWriter// rw 肯定实现了 Read 方法,所以它可以被视为 Readervarr Reader=rw// rw 肯定实现了 Write 方法,所以它可以被视为 Writervarw Writer=rw}这种就是不需要通过rw.(Reader)来断言,因为 ReadWriter 的定义里明确包含了 Reader,编译器在编译阶段就能 100% 确定:任何能放进 ReadWriter 变量的值,一定也能放进 Reader 变量。这是一种安全的、隐式的向上转型,类比成 java 就是下面的情况,只是 Java 中的接口不能实现接口:
publicclassMain{publicstaticvoidmain(String[]args){Ac1=newC();Bc2=newC();}}interfaceA{}interfaceB{}classCimplementsA,B{}可以看到 C 实现了 A、B 接口,所以 C 可以赋值给 A,也可以赋值给 B。
6. 切片类型转换的问题
下面先来看一个例子:
functest9(){varints[]int=[]int{1,2,3}// 下面这行会报错:Cannot use 'ints' (type []int) as the type []interface{}varinterfaces[]interface{}=ints}虽然单个int可以转成interface{},但是int切片就不行了,需要使用下面的写法:
functest9(){varints[]int=[]int{1,2,3}varinterfaces[]interface{}for_,v:=rangeints{// 每个 int 自动转为 interface{}interfaces=append(interfaces,v)}fmt.Println(interfaces[0].(int),interfaces[1].(int),interfaces[2].(int))// 1 2 3}🤔 为什么会这样?
- 🚀
[]int是一个连续内存块,每个元素是 固定大小的 int(比如 8 字节)。 - 🚀
[]interface{}每个元素是一个 接口值,包含两个指针:类型信息指针、数据指针,在 64 位系统上,每个 interface{} 占 16 字节,不能直接赋值。
7. 指针转换
还是一样先看下面的例子:
functest10(){variint32=0x12345678// 十六进制整数fmt.Printf("原始值: %x\n",i)// 原始值: 12345678// 获取指向 i 的 unsafe.Pointerp:=unsafe.Pointer(&i)// 转为 *byte 指针,访问第一个字节b:=(*byte)(p)// 修改第一个字节(小端序下是最低有效字节)*b=0xFF// 修改后: 123456fffmt.Printf("修改后: %x\n",i)}💡 在小端序机器上(如x86/x64),0x12345678 的内存布局是[78][56][34][12],修改第一个字节就是改0x78 → 0xFF,结果变成0x123456FF。
在 Go 中普通的指针比如*int、*string类型是安全的,因为:
*int只能指向int类型变量。- 不能把
*int直接赋给*float64,编译器会报错。
⚠️ 但是 unsafe.Pointer 比较特殊,是一个万能指针,可以指向任何类型的数据,同时也可以被强转成任何其他类型的指针,用不好会导致数据混乱,程序崩溃。
上面的代码首先获取了指向 i 的 unsafe.Pointer,然后转成 byte 指针,最后访问 byte 数据,修改第一个字节的地址。
最后再来介绍下uintptr,有时候需要对指针做算术运算(比如偏移几个字节),这时要用到 uintptr,如下面的例子,我们定义一个结构体,然后通过 uintptr 拿到结构体的地址,再通过算数运算进行访问和修改结构体里面的属性:
// 定义一个结构体typeDatastruct{IDint32// 占 4 字节Flagbool// 占 1 字节 (但通常会有填充 padding)Valueint64// 占 8 字节}functest11(){d:=Data{ID:100,Flag:false,Value:9999,}fmt.Printf("原始数据: %+v\n",d)fmt.Printf("原始 Value 地址: %p\n",&d.Value)// 1. 获取结构体起始地址,转成 unsafe.PointerbasePtr:=unsafe.Pointer(&d)// 2. 将 unsafe.Pointer 转成 uintptr, 整数, 可以进行指针地址加减baseAddr:=uintptr(basePtr)// 3. 获取 Value 这个字段的偏移量, 也就是结构体的起始位置 + x 才能到 Value 这个字段的位置offset:=unsafe.Offsetof(d.Value)// 4. 计算偏移量, 求出 Value 字段的地址targetAddr:=baseAddr+offset// 5. 转成指针valuePointer:=unsafe.Pointer(targetAddr)// 6. 转成具体类型的指针intPointer:=(*int64)(valuePointer)// 7. 访问 Value 值fmt.Println(strconv.FormatInt(int64(targetAddr),16),*intPointer)// 8. 修改值*intPointer=8888fmt.Println(d)// 最终输出如下:// 原始数据: {ID:100 Flag:false Value:9999}// 原始 Value 地址: 0xc00000a0a8// c00000a0a8 9999// {100 false 8888}}8. 小结
本文重点学习了 go 的类型转换,里面的例子使用千问 + 豆包生成,跟 Java 比起来还是很像的,下一篇文章就来学习下 go 的控制结构,也就是 if、for、switch 等。
如有错误,欢迎指出!!!!