Go基础:Go基本数据类型详解
文章目录
- 一、基本数据类型概述
- 1.1 基本数据类型的好处
- 1.1 基本数据类型的对比和总结
- 二、数字类型
- 2.1 整数类型
- 2.2 浮点类型
- 2.3 复数类型
- 三、布尔类型
- 四、字符串类型
- 4.1 核心概念
- 4.2 案例代码
- 五、类型转换
- 六、类型别名与类型定义
- 6.1 类型别名
- 6.2 类型定义
- 6.3 案例代码
一、基本数据类型概述
1.1 基本数据类型的好处
Go 语言作为一门静态类型、编译型的语言,其类型系统非常严谨和高效。理解基本数据类型是掌握 Go 编程的基石。在 Go 中,每个变量都有一个静态类型,这个类型在编译时就确定了,并且不能在运行时更改。这种设计带来了诸多好处:
- 性能:编译器可以生成高效的机器码,因为类型信息是已知的。
- 安全性:许多类型相关的错误可以在编译阶段就被发现,避免了运行时的崩溃。
- 可读性:明确的类型让代码的意图更加清晰,便于维护。
Go 的基本数据类型可以分为:数字类型(整数、浮点、复数)、布尔类型 和 字符串类型。
1.1 基本数据类型的对比和总结
类型类别 | 类型 | 关键点与最佳实践 |
---|---|---|
整数 | int , uint | 首选。除非有特殊需求(如内存优化、二进制协议、特定硬件交互),否则应使用 int 。 |
int64 , uint64 | 用于处理大数,例如 Unix 时间戳、文件大小、数据库 ID 等。 | |
byte (uint8 ) | 用于表示原始数据字节,如文件读写、网络通信。 | |
rune (int32 ) | 处理所有文本时必须使用。当你需要遍历字符串中的每个字符(尤其是非ASCII字符)时,使用 for...range 或 []rune 。 | |
浮点 | float64 | 默认选择。精度更高,性能与 float32 相当,适用于几乎所有场景。 |
float32 | 仅在内存占用是关键瓶颈且精度要求不高时使用(例如,机器学习中的某些模型权重)。 | |
复数 | complex128 | 默认选择,用于科学和工程计算。 |
布尔 | bool | 只能是 true 或 false 。在条件判断中,必须使用布尔表达式,禁止使用 if value 这样的“真值”判断。 |
字符串 | string | 理解其只读和UTF-8特性。用 len() 获取字节长度,用 utf8.RuneCountInString() 获取字符数量。大量拼接用 strings.Builder 。 |
转换 | 显式转换 T(v) | 永远记住 Go 没有隐式转换。不同类型间操作必须手动转换。数值和字符串转换使用 strconv 包。 |
定义 | type NewType Old | 用于创建具有特定行为(方法)的新类型,增强类型安全。 |
别名 | type Alias = Old | 用于重构或兼容,不创建新类型。 |
二、数字类型
2.1 整数类型
整数类型用于表示没有小数部分的数值。Go 提供了丰富的整数类型,分为有符号(可以表示正数、负数和零)和无符号(只能表示非负数)两大类。
1、有符号整数
类型 | 大小(字节) | 取值范围 | 描述 |
---|---|---|---|
int8 | 1 | -128 到 127 | 8位有符号整数 |
int16 | 2 | -32,768 到 32,767 | 16位有符号整数 |
int32 | 4 | -2,147,483,648 到 2,147,483,647 | 32位有符号整数 |
int64 | 8 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 64位有符号整数 |
int | 平台相关 | 32位系统上是 int32 ,64位系统上是 int64 | 最常用的通用整数类型,大小与操作系统位数一致 |
2、无符号整数
类型 | 大小(字节) | 取值范围 | 描述 |
---|---|---|---|
uint8 | 1 | 0 到 255 | 8位无符号整数,常用于表示RGB颜色值 |
uint16 | 2 | 0 到 65,535 | 16位无符号整数 |
uint32 | 4 | 0 到 4,294,967,295 | 32位无符号整数 |
uint64 | 8 | 0 到 18,446,744,073,709,551,615 | 64位无符号整数 |
uint | 平台相关 | 32位系统上是 uint32 ,64位系统上是 uint64 | 最常用的通用无符号整数类型 |
3、特殊整数类型
byte
:uint8
的别名,用于强调数据是字节(例如,处理二进制数据或 ASCII 字符)。byte
和uint8
可以互换使用。rune
:int32
的别名,用于表示一个 Unicode 码点。在处理多字节字符(如中文)时,必须使用rune
。
3、案例代码
package main
import ("fmt""unsafe" // 用于查看变量占用的字节数
)
func main() {// 1. 声明和初始化不同大小的整数var a int8 = 127var b int16 = -32768var c uint32 = 4294967295var d uint64 = 18446744073709551615fmt.Printf("a (int8): %d, 大小: %d 字节\n", a, unsafe.Sizeof(a))fmt.Printf("b (int16): %d, 大小: %d 字节\n", b, unsafe.Sizeof(b))fmt.Printf("c (uint32): %d, 大小: %d 字节\n", c, unsafe.Sizeof(c))fmt.Printf("d (uint64): %d, 大小: %d 字节\n", d, unsafe.Sizeof(d))// 2. 溢出问题// a = a + 1 // 编译错误!constant 128 overflows int8// Go 在编译时会检查字面量的溢出,但对于变量间的运算,可能会发生“回绕”var e int8 = 127e++fmt.Printf("e (int8) 溢出后: %d\n", e) // 输出 -128,这是预期的二进制补码行为// 3. int 和 uint 的大小var f intvar g uintfmt.Printf("f (int) 大小: %d 字节\n", unsafe.Sizeof(f)) // 在64位系统上输出 8fmt.Printf("g (uint) 大小: %d 字节\n", unsafe.Sizeof(g)) // 在64位系统上输出 8// 4. byte 和 runevar h byte = 'A' // 'A' 的 ASCII 码是 65var i rune = '世' // '世' 的 Unicode 码点是 U+4E16fmt.Printf("h (byte): %d, 字符: %c\n", h, h)fmt.Printf("i (rune): %d, 字符: %c, 大小: %d 字节\n", i, i, unsafe.Sizeof(i))// 5. 数字字面量// Go 支持多种进制表示j := 42 // 十进制k := 0b101010 // 二进制,以 0b 开头l := 052 // 八进制,以 0 开头m := 0x2A // 十六进制,以 0x 开头fmt.Printf("十进制: %d, 二进制: %b, 八进制: %o, 十六进制: %x\n", j, k, l, m)// 为了增加可读性,可以使用下划线 _ 分隔数字n := 1_000_000fmt.Printf("带下划线的数字: %d\n", n)
}
2.2 浮点类型
浮点类型用于表示带有小数部分的实数。Go 提供了两种浮点类型,遵循 IEEE 754 标准。
类型 | 大小(字节) | 描述 |
---|---|---|
float32 | 4 | 32位浮点数,精度约6-7位小数 |
float64 | 8 | 64位浮点数,精度约15-16位小数 |
1、重要提示
- 默认类型:在 Go 中,如果你声明一个浮点数变量而不指定类型(如
f := 3.14
),它默认是float64
。这是因为float64
在现代 CPU 上计算速度和float32
几乎一样快,但精度更高,是科学计算和大多数应用的首选。 - 精度问题:和所有浮点数实现一样,Go 的浮点数也存在精度损失。例如,
0.1
在计算机中无法被精确表示。
2、案例代码
package main
import ("fmt""math"
)
func main() {// 1. 声明和初始化var pi_float32 float32 = 3.1415926535var pi_float64 float64 = 3.14159265358979323846fmt.Printf("pi_float32: %.10f\n", pi_float32) // 输出: 3.1415927410 (精度损失)fmt.Printf("pi_float64: %.20f\n", pi_float64) // 输出: 3.14159265358979311600 (精度更高)// 2. 默认类型是 float64gravity := 9.81fmt.Printf("gravity 的类型是: %T\n", gravity) // 输出: gravity 的类型是: float64// 3. 浮点数精度问题a := 0.1b := 0.2c := a + bfmt.Printf("0.1 + 0.2 = %.18f\n", c) // 输出: 0.1 + 0.2 = 0.300000000000000044// 因此,永远不要用 == 来比较两个浮点数是否相等!// if c == 0.3 { ... } // 错误的做法// 正确的做法是判断它们之间的差值是否在一个极小的范围内(称为“epsilon”或“容差”)const epsilon = 1e-9if math.Abs(c-0.3) < epsilon {fmt.Println("c 约等于 0.3")}// 4. 科学计数法avogadro := 6.022e23 // 6.022 * 10^23planck := 6.626e-34 // 6.626 * 10^-34fmt.Printf("阿伏伽德罗常数: %e\n", avogadro)fmt.Printf("普朗克常数: %e\n", planck)// 5. math 包中的特殊值fmt.Printf("正无穷大: %f\n", math.Inf(1))fmt.Printf("负无穷大: %f\n", math.Inf(-1))fmt.Printf("非数值: %f\n", math.NaN())
}
2.3 复数类型
Go 是为数不多的在语言层面原生支持复数类型的通用编程语言之一。这对于科学计算、信号处理等领域非常方便。
类型 | 大小(字节) | 描述 |
---|---|---|
complex64 | 8 | 实部和虚部都是 float32 |
complex128 | 16 | 实部和虚部都是 float64 |
1、 创建和使用
- 可以使用内置的
complex()
函数创建复数:complex(realPart, imaginaryPart)
。 - 也可以直接使用字面量:
real + imagi
。
2、案例代码
package main
import ("fmt"
)
func main() {// 1. 使用 complex() 函数创建// 默认情况下,如果参数是浮点数字面量,会创建 complex128c1 := complex(3.0, 4.0)fmt.Printf("c1: %v, 类型: %T\n", c1, c1) // 输出: c1: (3+4i), 类型: complex128// 创建 complex64c2 := complex(float32(1.5), float32(2.5))fmt.Printf("c2: %v, 类型: %T\n", c2, c2) // 输出: c2: (1.5+2.5i), 类型: complex64// 2. 使用字面量创建c3 := 5 + 6ifmt.Printf("c3: %v, 类型: %T\n", c3, c3) // 输出: c3: (5+6i), 类型: complex128// 3. 提取实部和虚部// 使用内置的 real() 和 imag() 函数realPart := real(c1)imagPart := imag(c1)fmt.Printf("c1 的实部: %f, 虚部: %f\n", realPart, imagPart) // 输出: c1 的实部: 3.000000, 虚部: 4.000000// 4. 复数运算c4 := 1 + 2ic5 := 3 + 4isum := c4 + c5product := c4 * c5fmt.Printf("c4 + c5 = %v\n", sum) // 输出: c4 + c5 = (4+6i)fmt.Printf("c4 * c5 = %v\n", product) // 输出: c4 * c5 = (-5+10i)
}
三、布尔类型
布尔类型 bool
非常简单,它只有两个可能的值:true
和 false
。它主要用于逻辑判断和条件控制。
1、案例代码
package main
import "fmt"
func main() {// 1. 声明和初始化var isReady bool = truevar hasError bool = false// 类型推断isLoggedIn := truefmt.Printf("isReady: %t\n", isReady)fmt.Printf("hasError: %t\n", hasError)fmt.Printf("isLoggedIn: %t, 类型: %T\n", isLoggedIn, isLoggedIn)// 2. 逻辑运算符// && (与), || (或), ! (非)a := trueb := falsefmt.Printf("a && b: %t\n", a && b) // falsefmt.Printf("a || b: %t\n", a || b) // truefmt.Printf("!a: %t\n", !a) // false// 3. 在条件语句中使用age := 20if age >= 18 {fmt.Println("你是成年人。")} else {fmt.Println("你是未成年人。")}// 4. 重要:Go 中没有“真值”或“假值”的概念// 在很多语言中,0, "", nil 等在布尔上下文中被视为 false。// 但在 Go 中,这是不允许的。你必须使用明确的 bool 值。// var num int = 0// if num { // 编译错误:non-bool num (type int) used as if condition// }// 正确的做法是进行显式比较var num int = 0if num == 0 {fmt.Println("num 等于 0")}
}
四、字符串类型
字符串是 Go 中最重要、最常用的数据类型之一。理解其底层实现对于编写高效、正确的 Go 代码至关重要。
Go 语言中的字符串可以表示为任意的数据,比如以下代码,在 Go 语言中,字符串通过类型 string 声明:
var s1 string = "Hello"
var s2 string = "世界"
fmt.Println("s1 is",s1,",s2 is",s2)
fmt.Println("s1+s2=",s1+s2) // Hello世界
运行程序就可以看到打印的字符串结果。由于 s1 表示字符串“Hello”,s2 表示字符串“世界”,在终端输入 go run ch02/main.go 后,就可以打印出它们连接起来的结果“Hello世界。
4.1 核心概念
1、本质:一个 string
在 Go 中是一个只读的字节切片([]byte
)。这意味着字符串的内容一旦创建就不能被修改。
2、编码:Go 的字符串默认使用 UTF-8 编码。UTF-8 是一种可变长度的编码,可以表示世界上所有的 Unicode 字符。一个英文字符占用1个字节,一个中文字符通常占用3个字节。
len()
vsutf8.RuneCountInString()
:len(s)
: 返回字符串占用的字节数。utf8.RuneCountInString(s)
: 返回字符串中Unicode 字符(rune)的数量。
- 对于纯 ASCII 字符串,两者结果相同。对于包含多字节字符的字符串,后者才是我们通常理解的“长度”。
4.2 案例代码
package main
import ("fmt""unicode/utf8"
)
func main() {// 1. 声明和初始化s1 := "Hello, Go!" // 双引号,可以包含转义字符s2 := `这是一个
多行字符串` // 反引号,原生字符串,不处理转义符,可跨行fmt.Println(s1)fmt.Println(s2)// 2. 字符串的本质是字节切片s3 := "世界"fmt.Printf("字符串 s3: %s\n", s3)fmt.Printf("s3 的字节长度 (len): %d\n", len(s3)) // 输出: 6,因为 "世" 和 "界" 各占3个字节// 3. 遍历字符串// 错误的遍历方式:按字节遍历,会破坏多字节字符fmt.Println("--- 按字节遍历 (错误) ---")for i := 0; i < len(s3); i++ {fmt.Printf("%d: %x\n", i, s3[i])}// 正确的遍历方式:使用 for...range,它会自动按 rune 解码fmt.Println("--- 按 rune 遍历 (正确) ---")for i, r := range s3 {fmt.Printf("%d: %c (码点: %U)\n", i, r, r)}// 注意:这里的 i 是字符起始字节的索引,不是连续的 0, 1, 2...// 4. 获取字符数量(rune 数量)count := utf8.RuneCountInString(s3)fmt.Printf("s3 的字符数量: %d\n", count) // 输出: 2// 5. 字符串的不可变性// s3[0] = 'A' // 编译错误!cannot assign to s3[0]// 要修改字符串,必须先将其转换为可变的类型(如 []rune),修改后再转回 stringoriginal := "hello"runes := []rune(original)runes[0] = 'H'modified := string(runes)fmt.Printf("原始字符串: %s, 修改后: %s\n", original, modified)// 6. 字符串拼接str1 := "Go"str2 := "语言"result := str1 + str2fmt.Printf("拼接结果: %s\n", result) // Go语言// 对于大量拼接,使用 strings.Builder 更高效var builder strings.Builderfor i := 0; i < 5; i++ {builder.WriteString(str1)}fmt.Printf("高效拼接结果: %s\n", builder.String())
}
五、类型转换
Go 语言是强类型语言,并且不支持隐式类型转换。不同类型的变量之间赋值或运算,必须进行显式类型转换。这个规则虽然严格,但能有效避免很多因类型不匹配导致的潜在 bug。
1、语法
T(v)
// 将值 v 转换为类型 T
以字符串和数字互转这种最常见的情况为例,如下面的代码所示:
i2s:=strconv.Itoa(i)
s2i,err:=strconv.Atoi(i2s)
fmt.Println(i2s,s2i,err)
通过包 strconv 的 Itoa 函数可以把一个 int 类型转为 string,Atoi 函数则用来把 string 转为 int。
同理对于浮点数、布尔型,Go 语言提供了 strconv.ParseFloat、strconv.ParseBool、strconv.FormatFloat 和 strconv.FormatBool 进行互转。
对于数字类型之间,可以通过强制转换的方式,如以下代码所示:
i2f:=float64(i)
f2i:=int(f64)
fmt.Println(i2f,f2i)
这种使用方式比简单,采用“类型(要转换的变量)”格式即可。采用强制转换的方式转换数字类型,可能会丢失一些精度,比如浮点型转为整型时,小数点部分会全部丢失,你可以自己运行上述示例,验证结果。把变量转换为相应的类型后,就可以对相同类型的变量进行各种表达式运算和赋值了。
2、案例代码
package main
import "fmt"
func main() {var i int = 42var f float64 = 3.14// int -> float64f_int := float64(i)fmt.Printf("int %d 转换为 float64: %f\n", i, f_int)// float64 -> int (会截断小数部分,不是四舍五入)i_float := int(f)fmt.Printf("float64 %f 转换为 int: %d\n", f, i_float)// 不同类型之间的运算必须先转换// var sum int = i + f // 编译错误:invalid operation: mismatched types int and float64var sum float64 = float64(i) + ffmt.Printf("int + float64 的和: %f\n", sum)// 数值类型和 string 之间的转换// 不能直接转换!// var s string = string(42) // 这不会得到 "42",而是得到 '*' (ASCII 码为 42 的字符)// fmt.Println(s)// 正确的转换方式是使用 strconv 包import "strconv"num := 123strFromInt := strconv.Itoa(num) // Itoa = Integer to ASCIIfmt.Printf("int %d 转换为 string: \"%s\"\n", num, strFromInt)str := "456"intFromStr, err := strconv.Atoi(str) // Atoi = ASCII to Integerif err != nil {fmt.Println("转换失败:", err)} else {fmt.Printf("string \"%s\" 转换为 int: %d\n", str, intFromStr)}// float 和 string 的转换pi_str := "3.14159"pi_float, err := strconv.ParseFloat(pi_str, 64)if err != nil {fmt.Println("转换失败:", err)} else {fmt.Printf("string \"%s\" 转换为 float64: %f\n", pi_str, pi_float)}
}
六、类型别名与类型定义
Go 提供了两种创建新类型名称的方式,它们有本质区别。
6.1 类型别名
使用 =
号,它只是为现有类型创建了一个别名。别名和原类型是完全相同的,可以互相赋值。
type MyInt = int // MyInt 是 int 的一个别名
- 用途:主要用于大型代码库重构或兼容性处理。当你想给一个类型起一个更有意义的名字,但又不想让它成为一个全新的类型时,可以使用别名。
6.2 类型定义
不使用 =
号,它创建了一个全新的类型。新类型和原类型虽然底层结构相同,但它们是两种不同的类型,不能直接互相赋值。
type MyInt int // MyInt 是一个基于 int 的新类型
- 用途:用于增强代码的类型安全。你可以为新类型定义自己的方法,从而赋予它独特的行为。
6.3 案例代码
package main
import "fmt"
// 类型别名
type AliasInt = int
// 类型定义
type DefinedInt int
// 为新定义的类型添加方法
func (d DefinedInt) print() {fmt.Println("这是一个 DefinedInt 类型的值:", d)
}
func main() {var i int = 100var a AliasIntvar d DefinedInt// 1. 类型别名可以和原类型直接赋值a = i // OKi = a // OKfmt.Printf("int 和 AliasInt 可以互转: i=%d, a=%d\n", i, a)// 2. 类型定义不能和原类型直接赋值// d = i // 编译错误!cannot use i (type int) as type DefinedInt in assignment// 必须进行显式转换d = DefinedInt(i)fmt.Printf("int 转换为 DefinedInt: d=%d\n", d)// 3. 新类型可以有自己的方法d.print() // 调用 DefinedInt 的方法// 4. 类型别名没有引入新类型,所以它没有自己的方法// a.print() // 编译错误!a.print undefined (type AliasInt has no field or method print)
}
掌握这些基本数据类型及其特性,是编写出健壮、高效且符合 Go 语言设计哲学的代码的第一步。