Go 零基础教程

Go 基础数据类型

数据类型定义了变量可以存储什么样的数据以及可以对这些数据执行哪些操作。

本章将为你提供一个全面、通俗的 Go 语言基础数据类型概览。

我们将探索它们的内部特性、物理限制以及在日常开发中的最佳实践。

1. 整数 (Integers)

整数就是没有小数部分的完整数字(包括正数、负数和零)。Go 语言提供了极其丰富的整数类型,它们的主要区别在于占用的内存大小以及能表示的数值范围。根据你的实际需求挑选合适的整数类型,可以有效节省内存并防止数据溢出。

1.1 有符号整数 (Signed Integers)

有符号整数可以表示正数、负数和零。Go 提供了以下几种:

  • int8: 8 位有符号整数(取值范围:-128 到 127)
  • int16: 16 位有符号整数(取值范围:-32768 到 32767)
  • int32: 32 位有符号整数(取值范围:约 -21 亿到 21 亿)
  • int64: 64 位有符号整数(取值范围:极其巨大)
  • int: 最常用的默认整数类型。它的大小取决于你运行程序的电脑架构(在 32 位系统上是 32 位,在 64 位系统上是 64 位)。
package main
import "fmt"

func main() {
	var age int8 = 30           // 声明 8 位整数,适合存年龄
	var salary int32 = 50000    // 声明 32 位整数,适合存普通工资
	var population int64 = 7000000000 // 声明 64 位整数,存全球人口必须用它
	var score int = 100         // 声明平台相关的默认 int 类型
	
	fmt.Println("年龄:", age)
	fmt.Println("工资:", salary)
	fmt.Println("人口:", population)
	fmt.Println("分数:", score)
}

1.2 无符号整数 (Unsigned Integers)

无符号整数只能表示非负数(零和正数)。因为不需要用一位来存储正负号,所以它的正数表示上限比同位数的有符号整数大一倍。

  • uint8: 8 位无符号整数(0 到 255)
  • uint16: 16 位无符号整数(0 到 65535)
  • uint32: 32 位无符号整数(0 到约 42 亿)
  • uint64: 64 位无符号整数(极其巨大)
  • uint: 平台相关的无符号整数类型(32 位或 64 位)。
package main
import "fmt"

func main() {
	var positiveAge uint8 = 30        // 年龄不可能是负数,用 uint8 很合理
	var fileSize uint32 = 2000000     // 文件大小 (字节)
	var processID uint = 12345        // 进程 ID
	
	fmt.Println("正数年龄:", positiveAge)
	fmt.Println("文件大小:", fileSize)
	fmt.Println("进程 ID:", processID)
}

1.3 runebyte:两个特殊的别名

Go 语言为了处理字符和原始数据,提供了两个非常重要的类型别名:

  • rune: 它是 int32 的别名。在 Go 中,rune 专门用来表示一个 Unicode 码点 (Code Point)。当你处理包含中文、日文或 Emoji 等非 ASCII 字符的文本时,rune 是绝对的主力。
  • byte: 它是 uint8 的别名。通常用来表示原始的二进制数据块(单字节)。
package main
import "fmt"

func main() {
	var unicodeValue rune = '中' // 注意:字符用单引号包裹
	var dataByte byte = 255
	
	fmt.Println("字符 '中' 的 Unicode 码点数值:", unicodeValue) // 输出: 20013
	fmt.Println("数据字节:", dataByte) 
}

1.4 整数溢出与下溢 (Overflow and Underflow)

极其重要的一点:当赋予整数的值超出了它能容纳的极限时,Go 语言通常不会报错或崩溃 (Panic),而是会发生环绕 (Wrap around)

  • 溢出 (Overflow):比如 int8 最大是 127。如果你让 127 加 1,它不会变成 128,而是会“绕”回最小值,变成 -128。
  • 下溢 (Underflow):同理,-128 减 1 会变成 127。
package main
import "fmt"

func main() {
	var maxInt8 int8 = 127
	var minInt8 int8 = -128
	
	overflow := maxInt8 + 1
	underflow := minInt8 - 1
	
	fmt.Println("溢出结果:", overflow)   // 输出: -128
	fmt.Println("下溢结果:", underflow)  // 输出: 127
}
提示:在实际开发中,如果不确定数值范围,直接使用默认的 int 是最安全且最常见的做法。

2. 浮点数 (Floating-Point Numbers)

浮点数用于表示带有小数部分的数字,或者极其巨大/微小的数字。Go 提供了两种精度的浮点型:

  • float32: 32 位浮点数(单精度)。占用内存小,但精度较低(大约精确到小数点后 7 位)。
  • float64: 64 位浮点数(双精度)。精度高(大约精确到小数点后 15 位),是 Go 语言中浮点数的默认首选
package main
import "fmt"

func main() {
	var pi32 float32 = 3.14159
	var pi64 float64 = 3.14159265359
	
	fmt.Println("Float32 Pi:", pi32)
	fmt.Println("Float64 Pi:", pi64)
}

2.1 致命的浮点数精度陷阱

所有编程语言的浮点数底层都采用 IEEE 754 标准,这导致部分小数无法在计算机中被精确表示。这会引发经典的“精度丢失”问题:

package main
import "fmt"

func main() {
	var a float64 = 0.1
	var b float64 = 0.2
	var c float64 = a + b
	
	fmt.Println("0.1 + 0.2 的和:", c) // 输出警告: 0.30000000000000004
	
	if c == 0.3 {
		fmt.Println("相等")
	} else {
		fmt.Println("不相等!") // 程序会走到这里!千万不要用 == 直接比较浮点数
	}
}

正确比较浮点数的方法:
千万不要直接用 == 判断两个浮点数是否相等。你应该比较它们之间的差值绝对值是否小于一个极小的阈值。

package main

import (
	"fmt"
	"math"
)

func main() {
	var c float64 = 0.1 + 0.2
	var epsilon float64 = 1e-9 // 定义一个极小的容忍度阈值 (0.000000001)
	
    // math.Abs 用于计算绝对值:|c - 0.3| < epsilon
	if math.Abs(c-0.3) < epsilon {
		fmt.Println("它们近似相等,可以认为是相等的") 
	}
}

2.2 特殊的浮点数值

浮点数还可以表示几个特殊的数学概念,通过内置的 math 包可以获取:

  • NaN (Not a Number): 表示“不是一个数字”,比如 0 ÷ 0 的结果。注意:NaN 甚至不等于它自己。
  • Infinity (无穷大): 分为正无穷大 (+Inf) 和负无穷大 (-Inf)。

3. 布尔值 (Booleans)

布尔值代表逻辑上的真或假,在 Go 中用 bool 类型表示。它只有两个合法的值:true (真) 和 false (假)。

package main
import "fmt"

func main() {
	var isGoFun bool = true
	var isFishTasty bool = false
	
	fmt.Println("Go 好玩吗?", isGoFun)
}

布尔值是控制程序执行流(如 if 条件判断和循环)的绝对核心。

package main
import "fmt"

func main() {
	age := 20
	isStudent := true
	
    // && 表示逻辑“与”(且),! 表示逻辑“非”(取反)
	if age >= 18 && !isStudent {
		fmt.Println("符合全职工作条件")
	} else {
		fmt.Println("不符合条件") // 因为 isStudent 是 true,取反后为 false,整体条件不成立
	}
}

4. 字符串 (Strings)

字符串是一串字符的集合。在 Go 语言中,关于字符串有一个极其重要的核心概念:字符串是不可变的 (Immutable)。这意味着一个字符串一旦被创建,它在内存中的数据就绝对不能被修改。

package main
import "fmt"

func main() {
	var message string = "Hello, Go!"
	var emptyString string = "" // 空字符串
	fmt.Println(message)
}

4.1 常见的字符串操作

  • 拼接: 使用 + 运算符。
  • 获取长度: 使用内置的 len() 函数。(注意:它返回的是字节数,而不是字符数。对于纯英文字符串没区别,但对于包含中文的字符串要特别小心)。
  • 提取子串 (Slicing): 使用 [起始索引:结束索引] 的语法提取部分字符串。
package main
import "fmt"

func main() {
	message := "Hello, Go!"
	
	// 拼接
	newMessage := message + " 欢迎你!"
	
	// 获取长度
	length := len(message) // 长度为 10
	
	// 切片取子串 (获取索引 0 到 4 的内容,即前 5 个字母)
	substring := message[0:5]
	fmt.Println("提取的子串:", substring) // 输出: Hello
}

4.2 字符串的不可变性证明

如果你试图直接修改字符串里的某个字符,Go 编译器会毫不留情地报错:

package main
import "fmt"

func main() {
	message := "Hello"
	// message[0] = 'J' // 致命错误!编译器会阻拦你,因为字符串不可变
	
	// 正确的做法是:利用切片拼接,生成一个全新的字符串
	newMessage := "J" + message[1:] 
	fmt.Println(newMessage) // 输出: Jello
}

4.3 遍历中文字符串 (Rune 的威力)

如果你用普通的循环和索引去遍历一个包含中文的字符串,你会得到乱码的字节。要正确遍历多语言文本,必须使用 for...range 循环,它会自动把字符串解码为一个一个的 rune(Unicode 码点)。

package main
import "fmt"

func main() {
	message := "你好,世界!" 
	
    // index 是字节索引,runeValue 是解码后的 Unicode 字符
	for index, runeValue := range message {
        // %c 打印字符本身,%U 打印标准的 Unicode 格式代码
		fmt.Printf("索引位置: %d, 字符: %c, Unicode: %U\n", index, runeValue, runeValue)
	}
}

4.4 原生字符串字面量 (Raw Strings)

如果你想原封不动地保留多行文本格式,或者你的字符串里有大量需要转义的符号(如反斜杠 \、双引号 "),你可以使用反引号 ` 来包裹字符串。这种字符串被称为原生字符串,它会无视任何转义字符:

package main
import "fmt"

func main() {
	rawString := `这是一个原生字符串。
它可以随意跨越多行。
它不会理会 \n 或者 \t 这种转义符号,你输入什么样,它就打印什么样。
{"json": "甚至直接写 JSON 也不用转义双引号"}`

	fmt.Println(rawString)
}