Java 转 go 学习 - 类型转换

文章目录

  • 类型转换
    • [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 等。

如有错误,欢迎指出!!!!

相关推荐
小淮AI3 小时前
在职医生备考主治医师,我的资料搭配思路
学习
小小龙学IT3 小时前
Go 语言 encoding/json 标准库深度解析:从 Tag 反射到流式处理
golang·json
AOI小白新手上路4 小时前
韦东山《ARM 架构与编程》基于I.MX6ULL 3-1 硬件知识_LED 原理图 · 学习笔记
arm开发·学习·架构
传奇开心果编程5 小时前
【现代声明式UI学与练】第1课 从命令式UI到声明式UI
学习·flutter·ui·swiftui·react·android jetpack
老王爱玩车6 小时前
工具函数——清空输入缓冲区
c语言·开发语言·学习
JWASX8 小时前
Java 转 go 学习 - 函数(1)
学习·golang
sunshine22 girl8 小时前
Java学习五 面向对象高级5 内部类3-静态内部类和局部内部类(了解)
java·学习
小师兄吃牛肉9 小时前
什么是R语言?如何快速学习R语言
开发语言·学习·r语言