文章目录
- 类型转换
-
- [1. 数值类型之间的转换](#1. 数值类型之间的转换)
- [2. 字符串和字节切片](#2. 字符串和字节切片)
- [3. 字符串和数字之间的转换(最常用)](#3. 字符串和数字之间的转换(最常用))
- [4. 字符串和 bool 的转换](#4. 字符串和 bool 的转换)
- [5. 接口类型转换](#5. 接口类型转换)
-
- [5.1 接口类型断言](#5.1 接口类型断言)
- [5.2 接口 -> 接口转换](#5.2 接口 -> 接口转换)
- [6. 切片类型转换的问题](#6. 切片类型转换的问题)
- [7. 指针转换](#7. 指针转换)
- [8. 小结](#8. 小结)
本系列文章:
类型转换
1. 数值类型之间的转换
| 转换方向 | 说明 | 风险 |
|---|---|---|
int → int64 |
小转大 | ✅ 安全 |
int64 → int |
大转小 | ⚠️ 可能溢出 |
int → float64 |
整数转浮点 | ✅ 安全 |
float64 → int |
浮点转整数 | ⚠️ 丢失小数部分 |
int → byte |
转8位 | ⚠️ 可能溢出 |
例子如下:
go
package main
import "fmt"
func main() {
// 语法:目标类型(值)
var i int = 10
var f float64 = float64(i) // int → float64
var b byte = byte(i) // int → byte
var i64 int64 = int64(i) // int → int64
fmt.Println(f, b, i64)
// 10 10 10
// 精度丢失
var big int64 = 1000
var small int8 = int8(big)
fmt.Println(big, small)
// 1000 -24
// 浮点数转整数, 小数丢失
var float float64 = 10.5
fmt.Println(int(float))
// 10
}
2. 字符串和字节切片
其实就是 string 和 byte\[\] 的转换,类比 Java 的 String 和 byte\[\] 转换。
go
func test2() {
// 字符串 → []byte
s := "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 分配,直接追加到字节切片,高性能场景推荐。 |
下面简单看下一些例子:
go
func test4() {
// --- 数字转字符串 ---
n := 123
// 一般都是用这个函数, 转换最快, int -> string
s1 := 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)
if err1 == nil {
fmt.Println(val1)
// 456
}
// 需要处理 int64 或特定进制时使用, 第二个参数就是指定进制数
val2, err2 := strconv.ParseInt(s3, 2, 64)
fmt.Println(err2, val2)
// <nil> 1000
val3, err2 := strconv.ParseInt(s4, 8, 64)
fmt.Println(err2, val3)
// <nil> 1000
val4, 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 压力):
go
// 不推荐:每次循环都分配新的 string
for i := 0; i < 1000; i++ {
s := strconv.Itoa(i)
// ...
}
// 推荐直接操作 []byte
buf := make([]byte, 0, 64)
for i := 0; i < 1000; i++ {
buf = strconv.AppendInt(buf, int64(i), 10)
buf = append(buf, ',')
}
result := string(buf) // 最后再转一次 string
最后有一个注意点:
go
fmt.Println(string(65))
这行代码最终会输出 A,而不是 65,所以将 int 转 string,需要用 strconv.Itoa 或 fmt.Sprintf。
4. 字符串和 bool 的转换
ParseBool 可以将字符串转成 bool,支持: "1", "t", "T", "true", "TRUE", "True" 等字符串,下面是示例:
go
func test5() {
b := true
// bool -> string
s := 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 类,可以存储任何类型的数据,类型断言就是说:我知道这个接口变量里实际存的是某种具体类型,我要把它'还原'成那个具体类型,以便使用该类型特有的方法或属性。
🔹 语法如下:
go
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。 |
下面举个例子,首先是安全断言:
go
func test6() {
var i interface{} = "hello"
// 1. 断言为 string
s, ok := i.(string)
if ok {
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 判断是否断言不会程序异常,下面是直接断言,这种就比较危险,断言失败就会程序退出。
go
func test7() {
var i interface{} = "hello"
// 如果里面真的是 string,没问题
s := i.(string)
fmt.Println(s)
// 如果里面不是 int,下面这行会让程序直接崩溃 (panic)
n := i.(int)
// panic: interface conversion: interface {} is string, not int
fmt.Println(n)
}
当然如果不知道接口是什么类型,可以通过 switch 去判断:
go
func describe(i interface{}) {
// v 是根据 case 自动转换后的具体类型的值
switch v := i.(type) {
case int:
// 在这里 v 是 int 类型
fmt.Printf("整数: %d\n", v*2)
case string:
// 在这里 v 是 string 类型
fmt.Printf("字符串: %q\n", v)
case bool:
fmt.Printf("布尔: %v\n", v)
default:
// 未知类型,v 保持 interface{} 类型
fmt.Printf("未知类型: %T\n", v)
}
}
5.2 接口 -> 接口转换
这种接口到接口的转换就是 隐式类型 转换或接口赋值,比如下面的例子:
go
func test8(){
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
// ReadWriter 嵌入了 Reader 和 Writer,所以它必须同时实现 Read 和 Write
type ReadWriter interface { Reader; Writer }
// 这是一个大接口变量
var rw ReadWriter
// rw 肯定实现了 Read 方法,所以它可以被视为 Reader
var r Reader = rw
// rw 肯定实现了 Write 方法,所以它可以被视为 Writer
var w Writer = rw
}
这种就是不需要通过 rw.(Reader) 来断言,因为 ReadWriter 的定义里明确包含了 Reader,编译器在编译阶段就能 100% 确定:任何能放进 ReadWriter 变量的值,一定也能放进 Reader 变量。这是一种安全的、隐式的向上转型,类比成 java 就是下面的情况,只是 Java 中的接口不能实现接口:
java
public class Main {
public static void main(String[] args) {
A c1 = new C();
B c2 = new C();
}
}
interface A {}
interface B {}
class C implements A, B{
}
可以看到 C 实现了 A、B 接口,所以 C 可以赋值给 A,也可以赋值给 B。
6. 切片类型转换的问题
下面先来看一个例子:
go
func test9(){
var ints []int = []int{1, 2, 3}
// 下面这行会报错:Cannot use 'ints' (type []int) as the type []interface{}
var interfaces []interface{} = ints
}
虽然单个 int 可以转成 interface{},但是 int 切片就不行了,需要使用下面的写法:
go
func test9() {
var ints []int = []int{1, 2, 3}
var interfaces []interface{}
for _, v := range ints {
// 每个 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. 指针转换
还是一样先看下面的例子:
go
func test10() {
var i int32 = 0x12345678 // 十六进制整数
fmt.Printf("原始值: %x\n", i)
// 原始值: 12345678
// 获取指向 i 的 unsafe.Pointer
p := unsafe.Pointer(&i)
// 转为 *byte 指针,访问第一个字节
b := (*byte)(p)
// 修改第一个字节(小端序下是最低有效字节)
*b = 0xFF
// 修改后: 123456ff
fmt.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 拿到结构体的地址,再通过算数运算进行访问和修改结构体里面的属性:
go
// 定义一个结构体
type Data struct {
ID int32 // 占 4 字节
Flag bool // 占 1 字节 (但通常会有填充 padding)
Value int64 // 占 8 字节
}
func test11() {
d := Data{
ID: 100,
Flag: false,
Value: 9999,
}
fmt.Printf("原始数据: %+v\n", d)
fmt.Printf("原始 Value 地址: %p\n", &d.Value)
// 1. 获取结构体起始地址,转成 unsafe.Pointer
basePtr := 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 = 8888
fmt.Println(d)
// 最终输出如下:
// 原始数据: {ID:100 Flag:false Value:9999}
// 原始 Value 地址: 0xc00000a0a8
// c00000a0a8 9999
// {100 false 8888}
}
8. 小结
本文重点学习了 go 的类型转换,里面的例子使用千问 + 豆包生成,跟 Java 比起来还是很像的,下一篇文章就来学习下 go 的控制结构,也就是 if、for、switch 等。
如有错误,欢迎指出!!!!