变量和数据类型

Golang变量的使用

功能 说明 示例
声明变量 使用 var:= var a int = 10 / b := 20
匿名变量 使用 _ 忽略变量 _ , b := getValues()
命名风格 小写驼峰,导出用大写 userName, MaxValue
作用域 块级作用域 { var x int }
零值 未赋值时自动赋默认值 var s string => ""

变量声明与赋值

Go(Golang)是一门静态类型语言,变量在使用前必须声明。它对变量的声明和使用有明确的规范,语法简洁且安全。

1. 使用 var 声明变量(标准方式)

go
package main

import "fmt"

func main() {
    // 声明一个 int 类型变量 a
    var a int
    a = 10
    fmt.Println("a =", a)

    // 声明并初始化
    var b int = 20
    fmt.Println("b =", b)

    // 多个变量声明
    var x, y string
    x = "hello"
    y = "world"
    fmt.Println(x, y)
}

2. 类型推断(Type Inference)

Go 编译器可以根据赋值自动推断类型,所以你可以省略类型:

go
var c = 30
var d = "go"
fmt.Printf("Type of c is %T\n", c)
fmt.Printf("Type of d is %T\n", d)
// Type of c is int
// Type of d is string

查看数据类型

Go 语言中查看变量类型的方法是使用 %T 格式符。 %T 会输出变量的类型信息,这对于调试和理解代码的类型系统非常有帮助。

3. 简短声明(Short Variable Declaration)——最常用

使用 := 可以同时声明并赋值变量,只能在函数内部使用:

go
e := 42         // int
f := "Hello"    // string
g, h := true, 3.14 // bool, float64
fmt.Println(e, f, g, h)

Common Pitfall: := 不能用于已经声明过的变量 := 是一个声明操作符,不是赋值操作符。但有一个例外:当 := 左侧至少有一个新变量时,已存在的变量可以被重新赋值。

go
// 错误:one已经被声明
one := 0
one := 1 // 编译错误: no new variables on left side of :=

// 正确:two是新变量,允许one被重新赋值
one := 0
one, two := 1, 2 // 正确

// 实际应用:重复使用err变量
file, err := os.Open("a.txt")
content, err := ioutil.ReadAll(file) // err被重新赋值,而不是新声明

匿名变量

匿名变量(Blank Identifier):Go 中使用下划线 _ 表示“丢弃”的值,常用于忽略不需要使用的返回值或变量。

示例:忽略函数返回值

go
package main

import "fmt"

func getValues() (int, int) {
    return 100, 200
}

func main() {
    _, b := getValues() // 忽略第一个返回值
    fmt.Println("b =", b)
}

示例:忽略结构体字段遍历中的索引

go
nums := []int{1, 2, 3}
for _, num := range nums {
    fmt.Println(num)
}

✅ 匿名变量 _ 不会分配内存,也不会被访问,是 Go 中避免“未使用变量”错误的一种优雅方式。


变量命名规范

Go 社区有一套清晰的变量命名规范(Naming Convention),建议遵循以下原则:

1. 小写驼峰命名法(camelCase)

  • 推荐用于变量、函数、方法等。
  • 示例:
    go
    var userName string
    var maxValue int

2. 首字母大写表示导出(Exported)

  • 如果你希望变量、函数、结构体等可以在其他包中使用,首字母要大写。
  • 示例:
    go
    package utils
    
    var MaxValue int = 100 // 可被其他包访问
    var minValue int = 50  // 仅当前包可访问

3. 简短而有意义

  • Go 鼓励使用简短但具有描述性的名称。
  • 示例:
    go
    i := 0           // 在循环中可以接受
    count := 10      // 更具语义
    name := "Alice"  // 清晰直观

4. 不要使用下划线命名法(snake_case)

虽然 Go 允许使用 snake_case,但社区更推荐使用 camelCase


变量作用域

Go 支持块级作用域(block scope),即变量只在声明它的 {} 内部有效:

go
func main() {
    if true {
        insideIf := "I'm inside if"
        fmt.Println(insideIf)
    }

    // 下面这行会报错:undefined: insideIf
    // fmt.Println(insideIf)
}

Common Pitfall: 变量覆盖 (Shadowing) 在一个新的作用域(如 iffor 块)中使用 := 可能会无意中创建一个同名的新变量,而不是修改外部作用域的变量。这是一种非常常见且难以发现的错误。

go
var client *http.Client // 全局或外部变量

if someCondition {
    client, err := createClientWithProxy() // 错误!这里的client是新声明的局部变量
    if err != nil {
        // ...
    }
    // 此if块结束后,这个新的client就被销毁了
}

// 这里的client可能仍然是nil,因为它没有被赋值
client.Get("...") // 可能导致 panic: nil pointer dereference

// 正确做法:使用 = 进行赋值
var err error
if someCondition {
    client, err = createClientWithProxy() // 正确,给外部的client赋值
    if err != nil {
        // ...
    }
}

Best Practice: 可以使用 go vet -shadow 工具来检查代码中潜在的变量覆盖问题。


常量 (Constants)

常量使用 const 关键字定义。常量的值在编译时确定,不能在运行时修改。常量可以是 有类型的 (typed)无类型的 (untyped)

  • 无类型常量: const Pi = 3.14159。它们具有“理想的”数字类型,可以根据上下文自动转换为所需的类型,直到被用于需要特定类型的操作或赋值给一个有类型的变量。这提供了很大的灵活性。
  • 有类型常量: const TypedPi float64 = 3.14159。它们具有明确的类型,并且只能赋值给相同类型的变量。

常量可以使用 iota 生成器,iota 在每个 const 声明块中从 0 开始,每增加一个常量声明(通常是换行)则自动加 1。

go
package main

import "fmt"

const UntypedPi = 3.1415926535 // 无类型浮点常量
const TypedPi float64 = 3.1415926535 // 有类型浮点常量

const (
    Big = 1 << 100 // 无类型整数常量,非常大
    Small = Big >> 99 // 1 << 1 = 2
)

const (
    C0 = iota // 0
    C1 = iota // 1
    C2 = iota // 2
)

const (
    Red = iota // 0
    Green      // 1 (隐式使用 iota)
    Blue       // 2 (隐式使用 iota)
)

func needInt(x int) int { return x * 10 }
func needFloat64(x float64) float64 { return x * 0.1 }

func main() {
    var f32 float32 = UntypedPi // 无类型常量可以赋给 float32 (如果值在范围内)
    // var f32_2 float32 = TypedPi // 编译错误: cannot use TypedPi (type float64) as type float32 in assignment

    fmt.Println(f32)
    fmt.Println(TypedPi)

    fmt.Println(needInt(Small)) // Untyped Small (2) 隐式转换为 int
    fmt.Println(needFloat64(Small)) // Untyped Small (2) 隐式转换为 float64
    // fmt.Println(needInt(Big)) // 编译错误: constant 126765... overflows int

    const Greeting = "Hello" // 无类型字符串常量
    var s string = Greeting  // 可以赋值给 string
    fmt.Println(s)

    fmt.Printf("C0: %d, C1: %d, C2: %d\n", C0, C1, C2)
    fmt.Printf("Red: %d, Green: %d, Blue: %d\n", Red, Green, Blue)
}

// 输出:
// 3.1415927
// 3.1415926535
// 20
// 0.2
// Hello
// C0: 0, C1: 1, C2: 2
// Red: 0, Green: 1, Blue: 2

零值 (Zero Values)

Go 中的每个类型都有一个零值。当一个变量被声明但没有显式初始化时,它会被自动赋予其类型的零值。

类型 零值
bool false
int, int8, int16, int32, int64 0
uint, uint8, uint16, uint32, uint64, uintptr 0
float32, float64 0.0
complex64, complex128 0+0i
string "" (空字符串)
pointer nil
interface nil
slice nil
map nil
channel nil
function nil
array 数组中每个元素的零值
struct 结构体中每个字段的零值

示例代码:

go
package main

import "fmt"

func main() {
	var i int
	var f float64
	var b bool
	var s string
	var p *int
    var sl []int
    var m map[string]int
    var arr [3]int
    type Person struct{ Name string; Age int }
    var person Person

	fmt.Printf("int: %d\n", i)
	fmt.Printf("float64: %f\n", f)
	fmt.Printf("bool: %t\n", b)
	fmt.Printf("string: '%s'\n", s)
	fmt.Printf("pointer: %v\n", p)
    fmt.Printf("slice: %v, is nil? %t\n", sl, sl == nil)
    fmt.Printf("map: %v, is nil? %t\n", m, m == nil)
    fmt.Printf("array: %v\n", arr)
    fmt.Printf("struct: %+v\n", person)
}

// 输出:
// int: 0
// float64: 0.000000
// bool: false
// string: ''
// pointer: <nil>
// slice: [], is nil? true
// map: map[], is nil? true
// array: [0 0 0]
// struct: {Name: Age:0}

Common Pitfall: nil 接口、切片和Map的行为差异

  1. 接口 (interface): 一个 nil 接口值 (var i interface{}) 和一个持有 nil 指针的接口值 (var p *int; i = p) 是不相等的。前者类型和值都为 nil,后者类型为 *int,值为 nilif i != nil 在后一种情况下为 true,是常见的错误源。
  2. 切片 (slice): 对一个 nil 切片执行 len(), cap(), for rangeappend 操作都是安全的。
  3. Map: 对一个 nil map 执行 len(), for range 或读取操作是安全的(读取会返回零值)。但是,向一个 nil map 写入数据会立即引发 panic

Golang数据类型及转换

Go语言(又称Golang)是一种静态类型语言,具有丰富的数据类型系统。

  • Go 的基本数据类型 (Basic Types):布尔型、数字类型和字符串
  • Go 的复合数据类型:数组、切片、Map

布尔型 (Boolean)

布尔型只有两个值:truefalse默认值: false

go
package main

import "fmt"

func main() {
    var isEnabled bool = true
    var isUserAdmin bool // 默认值为 false

    fmt.Printf("isEnabled: %t\n", isEnabled)
    fmt.Printf("isUserAdmin: %t\n", isUserAdmin)

    canProceed := isEnabled && !isUserAdmin
    fmt.Printf("Can proceed? %t\n", canProceed)
}

// 输出:
// isEnabled: true
// isUserAdmin: false
// Can proceed? true

数字类型

数字类型 (Numeric Types)分为整型(Integers)、浮点型(Floating-Point)和复数(Complex)。

整型 (Integers)

Go 提供了多种尺寸的整型,分为有符号(int)和无符号(uint)。

类型 大小 范围 说明
int8 8位 -128 到 127
int16 16位 -32768 到 32767
int32 32位 -21亿 到 21亿 别名 rune,用于表示一个 Unicode 码点
int64 64位 约 -9e18 到 9e18
uint8 8位 0 到 255 别名 byte,常用于处理原始数据
uint16 16位 0 到 65535
uint32 32位 0 到 42亿
uint64 64位 0 到 约 1.8e19
int 32或64位 依赖于操作系统架构 在 64 位系统上是 64 位,32 位系统上是 32 位
uint 32或64位 依赖于操作系统架构 同上
uintptr 无符号整型,足以存储一个指针的位
go
package main

import "fmt"

func main() {
    var a int8 = -10
    var b uint16 = 200

    // int 会根据你的系统架构决定大小
    var defaultInt int = 1234567890
    
    // byte 是 uint8 的别名
    var c byte = 'A' // 存储的是字符'A'的ASCII码

    // rune 是 int32 的别名
    var d rune = '你' // 存储的是'你'的Unicode码点

    fmt.Printf("a (int8): %d\n", a)
    fmt.Printf("b (uint16): %d\n", b)
    fmt.Printf("defaultInt (int): %d\n", defaultInt)
    fmt.Printf("c (byte): %d, 对应字符: %c\n", c, c)
    fmt.Printf("d (rune): %d, 对应字符: %c\n", d, d)
}

// 输出:
// a (int8): -10
// b (uint16): 200
// defaultInt (int): 1234567890
// c (byte): 65, 对应字符: A
// d (rune): 20320, 对应字符: 你

浮点型 (Floating-Point)

类型 大小 说明
float32 32位 单精度浮点数
float64 64位 双精度浮点数(默认)
go
package main

import "fmt"

func main() {
    var pi32 float32 = 3.14159
    var pi64 float64 = 3.141592653589793

    // 如果不指定类型,默认推断为 float64
    pi := 3.14 

    fmt.Printf("pi32: %f\n", pi32)
    fmt.Printf("pi64: %.10f\n", pi64)
    fmt.Printf("pi (默认类型是 float64): %f\n", pi)
}

// 输出:
// pi32: 3.141590
// pi64: 3.1415926536
// pi (默认类型是 float64): 3.140000

复数 (Complex)

Go 也内置了对复数的支持。

类型 大小 说明
complex64 实部和虚部都是 float32
complex128 实部和虚部都是 float64
go
package main

import "fmt"

func main() {
    var c1 complex64 = 5 + 10i
    c2 := complex(3, 4) // complex128

    fmt.Printf("c1 (complex64): %v\n", c1)
    fmt.Printf("c2 (complex128): %v\n", c2)
    fmt.Printf("Real part of c2: %f\n", real(c2))
    fmt.Printf("Imaginary part of c2: %f\n", imag(c2))
}
// 输出:
// c1 (complex64): (5+10i)
// c2 (complex128): (3+4i)
// Real part of c2: 3.000000
// Imaginary part of c2: 4.000000

字符串 (String)

Go 中的字符串是不可变的,并且默认使用 UTF-8 编码。 默认值: "" (空字符串)

字符串是Go语言中专门设计的、不可变的字节序列类型,主要用于表示UTF-8编码的文本

go
package main

import "fmt"

func main() {
    s1 := "Hello, World!"
    s2 := "你好, 世界!"

    // 使用反引号创建多行字符串
    s3 := `这是
一个
多行字符串。`

    fmt.Println(s1)
    fmt.Println(s2)
    fmt.Println(s3)

    // 字符串是不可变的,下面这行代码会编译错误
    // s1[0] = 'h' 

    // 遍历字符串
    for i, r := range s2 { // r 是 rune 类型
        fmt.Printf("Index: %d, Rune: %c, Unicode: %U\n", i, r, r)
    }
}

// 输出:
// Hello, World!
// 你好, 世界!
// 这是
// 一个
// 多行字符串。
// Index: 0, Rune: 你, Unicode: U+4F60
// Index: 3, Rune: 好, Unicode: U+597D
// Index: 6, Rune: ,, Unicode: U+002C
// Index: 7, Rune:  , Unicode: U+0020
// Index: 8, Rune: 世, Unicode: U+4E16
// Index: 11, Rune: 界, Unicode: U+754C
// Index: 14, Rune: !, Unicode: U+FF01

注意遍历中文字符串时,索引 i 是字节索引,并非字符索引。


Go 的 string 类型本身是不可变的,相关操作函数主要位于 strings 包。

函数 (示例) 说明
strings.Contains(s, substr string) bool 判断字符串 s 是否包含子串 substr
strings.Count(s, substr string) int 计算字符串 s 中包含子串 substr 的数量。
strings.HasPrefix(s, prefix string) bool 判断字符串 s 是否以 prefix 开头。
strings.HasSuffix(s, suffix string) bool 判断字符串 s 是否以 suffix 结尾。
strings.Index(s, substr string) int 返回子串 substr 在字符串 s 中第一次出现的位置,如果不存在则返回 -1。
strings.LastIndex(s, substr string) int 返回子串 substr 在字符串 s 中最后一次出现的位置,如果不存在则返回 -1。
strings.Join(a []string, sep string) string 将字符串切片 a 中的所有元素用 sep 连接成一个字符串。
strings.Split(s, sep string) []string 将字符串 s 按照 sep 分割成字符串切片。
strings.ToLower(s string) string 将字符串 s 转换为小写。
strings.ToUpper(s string) string 将字符串 s 转换为大写。
strings.TrimSpace(s string) string 去除字符串 s 首尾的空白字符。
strings.Trim(s, cutset string) string 去除字符串 s 首尾包含在 cutset 中的字符。
strings.Replace(s, old, new string, n int) string 将字符串 s 中的 old 子串替换为 new 子串,n 指定替换次数(-1 表示全部替换)。
strings.NewReplacer(oldnew ...string) *strings.Replacer 创建一个 Replacer 对象,可以进行多次高效的替换。
go
package main

import (
	"fmt"
	"strings"
)

func main() {
	s := "  Hello, Gopher! Gopher is great.  "
	fmt.Printf("Original: '%s'\n", s)

	fmt.Printf("Contains 'Gopher': %t\n", strings.Contains(s, "Gopher"))
	fmt.Printf("Count 'Gopher': %d\n", strings.Count(s, "Gopher"))
	fmt.Printf("HasPrefix '  Hello': %t\n", strings.HasPrefix(s, "  Hello"))
	fmt.Printf("Index of 'Gopher': %d\n", strings.Index(s, "Gopher"))

	words := []string{"Go", "is", "awesome"}
	fmt.Printf("Joined: '%s'\n", strings.Join(words, "-"))

	data := "apple,banana,orange"
	fruits := strings.Split(data, ",")
	fmt.Printf("Split: %v\n", fruits)

	fmt.Printf("ToLower: '%s'\n", strings.ToLower(s))
	fmt.Printf("TrimSpace: '%s'\n", strings.TrimSpace(s))
	fmt.Printf("ReplaceAll: '%s'\n", strings.ReplaceAll(s, "Gopher", "GoLang"))
}

// 输出:
// Original: '  Hello, Gopher! Gopher is great.  '
// Contains 'Gopher': true
// Count 'Gopher': 2
// HasPrefix '  Hello': true
// Index of 'Gopher': 9
// Joined: 'Go-is-awesome'
// Split: [apple banana orange]
// ToLower: '  hello, gopher! gopher is great.  '
// TrimSpace: 'Hello, Gopher! Gopher is great.'
// ReplaceAll: '  Hello, GoLang! GoLang is great.  '

字符串与字节切片

字符串是独立内置类型,用于表示文本(默认UTF-8编码)。而字节切片设计用于表示原始二进制数据

  • 字符串和[]byte在内存中都是连续的字节序列

  • 都可以通过索引访问单个字节

  • 可以相互转换

    go
    s := "hello"
    b := []byte(s)  // 字符串转字节切片
    s2 := string(b) // 字节切片转字符串
  • 长度属性:都有len()函数可以获取长度(字节数)

特性 字符串(string) 字节切片([]byte)
可变性 不可变(只读) 可变(可修改元素)
类型系统 内置类型 切片类型
UTF-8特性 设计用于文本,支持Unicode 纯字节序列
nil值 空字符串"",不能为nil 可以为nil
内存布局 头部包含长度和指针 切片头包含指针、长度和容量

bytes 包提供了许多与 strings 包功能类似的函数,但它们操作的是字节切片 ([]byte)。

一些常用函数(与 strings 包对应):

函数 (示例) - bytes 说明
bytes.Contains(b, subslice []byte) bool 判断 b 是否包含子切片 subslice
bytes.Count(b, sep []byte) int 计算 bsep 出现的次数。
bytes.HasPrefix(b, prefix []byte) bool 判断 b 是否以 prefix 开头。
bytes.Join(s [][]byte, sep []byte) []byte 连接字节切片。
bytes.Split(s, sep []byte) [][]byte 分割字节切片。
bytes.ToLower(s []byte) []byte 转为小写。
bytes.TrimSpace(s []byte) []byte 去除首尾空白。
bytes.Replace(s, old, new []byte, n int) []byte 替换。
bytes.Buffer 一个可变长度的字节缓冲区,提供了 ReadWrite 方法,常用于高效构建字节序列。

示例代码 (bytes.Buffer):

go
package main

import (
	"bytes"
	"fmt"
)

func main() {
	var buf bytes.Buffer // 创建一个Buffer

	buf.WriteString("Hello, ")
	buf.WriteByte('W')
	buf.Write([]byte("orld!"))

	fmt.Println(buf.String()) // 输出: Hello, World!

	// 从 Buffer 读取
	data := make([]byte, 5)
	n, _ := buf.Read(data) // 读取前5个字节
	fmt.Printf("Read %d bytes: %s\n", n, data[:n]) // Read 5 bytes: Hello
	fmt.Printf("Remaining in buffer: %s\n", buf.String()) // Remaining in buffer: , World!
}
// 输出:
// Hello, World!
// Read 5 bytes: Hello
// Remaining in buffer: , World!

复合数据类型

Go 的复合数据类型 (Composite Types)主要包括:

  • 数组 (Array): [n]T 固定长度的同一类型元素序列。长度是类型的一部分。
  • 切片 (Slice): []T 动态长度的同一类型元素序列,是对数组的引用(或一部分)。更常用。
  • Map: map[K]V 键值对集合,键类型 K 必须是可比较的。
  • 结构体 (Struct): struct 用户自定义的字段集合,每个字段可以有不同的类型。
  • 指针 (Pointer): *T 存储另一个变量的内存地址。
  • 接口 (Interface): interface 定义了一组方法签名。类型通过实现接口的所有方法来满足该接口。
  • 通道 (Channel): chan T 用于在 Goroutine 之间安全地传递数据,实现并发通信。
go
package main

import "fmt"

// 结构体
type Point struct {
    X, Y int
}

// 接口
type Shaper interface {
    Area() float64
}

func main() {
    // 数组
    var arr [3]int = [3]int{1, 2, 3}
    fmt.Println("Array:", arr)

    // 切片
    slice := []int{10, 20, 30, 40}
    fmt.Println("Slice:", slice)

    // Map
    m := make(map[string]int)
    m["one"] = 1
    m["two"] = 2
    fmt.Println("Map:", m)

    // 结构体实例
    pt := Point{X: 5, Y: 10}
    fmt.Printf("Point: %+v\n", pt)

    // 指针
    num := 42
    ptr := &num
    fmt.Printf("Pointer %p points to value %d\n", ptr, *ptr)

    // 通道 (更多在并发中使用)
    // ch := make(chan int)
}
// 输出:
// Array: [1 2 3]
// Slice: [10 20 30 40]
// Map: map[one:1 two:2]
// Point: {X:5 Y:10}
// Pointer 0xc000018030 points to value 42 (地址会变化)

类型转换

Go 中没有隐式 类型转换 (Type Conversion),所有的转换都必须是显式的。

语法: T(v),其中 T 是目标类型,v 是要转换的值。

数字类型之间的转换

不同数字类型之间不能直接进行算术运算,必须先转换成相同类型。

go
package main

import "fmt"

func main() {
    var i int = 100
    var u uint = uint(i) // int 转换为 uint
    var f float64 = float64(i) // int 转换为 float64

    fmt.Printf("i (int): %d\n", i)
    fmt.Printf("u (uint): %d\n", u)
    fmt.Printf("f (float64): %.2f\n", f)

    var fValue float64 = 3.14
    // 将 float64 转换为 int 时,小数部分会被截断
    var iValue int = int(fValue) 

    fmt.Printf("fValue (float64): %.2f\n", fValue)
    fmt.Printf("iValue (int from float64): %d\n", iValue)
    
    // 不同类型不能直接运算,下面这行会编译错误
    // sum := i + fValue
    
    // 必须先转换
    sum := float64(i) + fValue
    fmt.Printf("Sum: %.2f\n", sum)
}

// 输出:
// i (int): 100
// u (uint): 100
// f (float64): 100.00
// fValue (float64): 3.14
// iValue (int from float64): 3
// Sum: 103.14

注意: 从高精度类型(如 float64)向低精度类型(如 int)转换时,可能会导致精度丢失或值溢出。从有符号到无符号(或反之)也需注意值的范围。


数字与字符串之间的转换

数字和字符串之间的转换需要使用 strconv 包。

转换方向 方法/函数 注意事项
int -> string strconv.Itoa(i int) strconv Itoa = Integer to ASCII
string -> int strconv.Atoi(s string) strconv 返回 (int, error),需处理错误
int64 -> string strconv.FormatInt(i int64, base int) strconv base 通常是 10
string -> int64 strconv.ParseInt(s string, base int, bitSize int) strconv 返回 (int64, error),需处理错误
float -> string strconv.FormatFloat(f float64, fmt byte, prec, bitSize int) strconv 参数复杂,功能强大
string -> float strconv.ParseFloat(s string, bitSize int) strconv 返回 (float64, error),需处理错误
bool -> string strconv.FormatBool(b bool) strconv
string -> bool strconv.ParseBool(s string) strconv 返回 (bool, error)
go
package main

import (
	"fmt"
	"strconv"
)

func main() {
	// 1. int -> string
	num := 123
	strNum := strconv.Itoa(num)
	fmt.Printf("从 int 转换: 值='%s', 类型=%T\n", strNum, strNum)

	// 2. string -> int
	str := "456"
	numFromStr, err := strconv.Atoi(str)
	if err != nil {
		fmt.Println("Atoi 转换失败:", err)
	} else {
		fmt.Printf("从 string 转换: 值=%d, 类型=%T\n", numFromStr, numFromStr)
	}
    
    invalidStr := "hello"
    _, err = strconv.Atoi(invalidStr)
    if err != nil {
        fmt.Printf("'%s' 无法转换为 int: %v\n", invalidStr, err)
    }

	// 3. float64 -> string
	f := 3.14159
	// 'f' 表示格式,-1 表示尽可能精确(但通常指定小数位数),64 表示 float64
	strFloat := strconv.FormatFloat(f, 'f', 2, 64) // 保留2位小数
	fmt.Printf("从 float64 转换: 值='%s', 类型=%T\n", strFloat, strFloat)

	// 4. string -> float64
	strF := "2.718"
	floatFromStr, err := strconv.ParseFloat(strF, 64) // 64 表示目标是 float64
	if err != nil {
		fmt.Println("ParseFloat 转换失败:", err)
	} else {
		fmt.Printf("从 string 转换: 值=%f, 类型=%T\n", floatFromStr, floatFromStr)
	}

    // 5. bool -> string
    bStr := strconv.FormatBool(true)
    fmt.Printf("从 bool 转换: 值='%s', 类型=%T\n", bStr, bStr)

    // 6. string -> bool
    strB := "true"
    bFromStr, err := strconv.ParseBool(strB)
    if err != nil {
        fmt.Println("ParseBool 转换失败:", err)
    } else {
        fmt.Printf("从 string 转换: 值=%t, 类型=%T\n", bFromStr, bFromStr)
    }
}

// 输出:
// 从 int 转换: 值='123', 类型=string
// 从 string 转换: 值=456, 类型=int
// 'hello' 无法转换为 int: strconv.Atoi: parsing "hello": invalid syntax
// 从 float64 转换: 值='3.14', 类型=string
// 从 string 转换: 值=2.718000, 类型=float64
// 从 bool 转换: 值='true', 类型=string
// 从 string 转换: 值=true, 类型=bool

字符串与字节切片 ([]byte) 的转换

这是一种非常常见的转换,因为很多 I/O 操作都使用字节切片。

go
package main

import "fmt"

func main() {
	s := "Hello, Go!"

	// string -> []byte
	byteSlice := []byte(s)
	fmt.Printf("字符串 '%s' -> 字节切片: %v (ASCII: %d %d %d...)\n", s, byteSlice, byteSlice[0], byteSlice[1], byteSlice[2])

	// []byte -> string
	newString := string(byteSlice)
	fmt.Printf("字节切片 %v -> 字符串: '%s'\n", byteSlice, newString)
    
    // 修改字节切片不会影响原字符串(因为字符串是不可变的)
    // 但从字节切片创建新字符串后,新字符串与原字符串也无关
    byteSlice[0] = 'h' // 修改字节切片的内容
    fmt.Printf("修改后的字节切片: %v\n", byteSlice)
    fmt.Printf("原始字符串 s 仍然是: '%s'\n", s) // s 不变
    fmt.Printf("从修改后的字节切片生成的新字符串: '%s'\n", string(byteSlice)) // 新字符串反映了修改
}

// 输出:
// 字符串 'Hello, Go!' -> 字节切片: [72 101 108 108 111 44 32 71 111 33] (ASCII: 72 101 108...)
// 字节切片 [72 101 108 108 111 44 32 71 111 33] -> 字符串: 'Hello, Go!'
// 修改后的字节切片: [104 101 108 108 111 44 32 71 111 33]
// 原始字符串 s 仍然是: 'Hello, Go!'
// 从修改后的字节切片生成的新字符串: 'hello, Go!'

性能提示: string[]byte[]bytestring 的转换会发生内存拷贝。对于高性能场景,需注意其开销。


类型断言与类型选择

类型断言 (Type Assertion) 与类型选择 (Type Switch)主要用于接口类型 interface{}。当有一个接口类型的值,但想知道它底层存储的具体类型,并希望使用该具体类型的值时,就需要用到类型断言或类型选择。

类型断言

语法: value.(T)

这会尝试将接口值 value 转换为具体类型 T

  • 如果 value 的底层动态类型确实是 T,转换成功,返回 T 类型的值。
  • 如果 value 的底层动态类型不是 T,程序会 panic(崩溃)。
  • 如果 valuenil 接口值,也会 panic

为了安全地进行断言,可以使用“comma, ok”范式:v, ok := value.(T)

  • 如果转换成功,oktruev 是转换后的 T 类型的值。
  • 如果失败(类型不匹配或 valuenil),okfalsevT 类型的零值,程序不会 panic
go
package main

import "fmt"

func main() {
    var i interface{} = "hello"

    // 安全的类型断言
    s, ok := i.(string)
    if ok {
        fmt.Printf("断言成功: s = '%s'\n", s)
    } else {
        fmt.Println("断言为 string 失败")
    }

    // 尝试断言为 int (会失败)
    n, ok := i.(int)
    if ok {
        fmt.Printf("断言成功: n = %d\n", n)
    } else {
        fmt.Printf("断言为 int 失败, n (int的零值) 为: %v, ok 的值为: %t\n", n, ok)
    }

    // 不安全的类型断言 (如果类型不匹配会 panic)
    // f := i.(float64) // 这行代码会引发 panic: interface conversion: interface {} is string, not float64
    // fmt.Println(f)

    var x interface{} = 10
    num := x.(int) // 安全,因为 x 的动态类型是 int
    fmt.Printf("x 断言为 int: %d\n", num)

    var y interface{} // y 是 nil 接口
    // z, ok := y.(int) // ok 会是 false, z 会是 0
    // fmt.Printf("nil interface断言: z=%v, ok=%t\n", z, ok)
    // y.(int) // 这行会 panic: interface conversion: interface {} is nil, not int
}

// 输出:
// 断言成功: s = 'hello'
// 断言为 int 失败, n (int的零值) 为: 0, ok 的值为: false
// x 断言为 int: 10

Common Pitfall: 失败的类型断言和变量覆盖 当使用 if data, ok := data.(int) 这种形式时,如果断言失败,else 块中的 data 将是新声明的、类型为 int 且值为 0data,而不是原始的接口变量 data。这会覆盖原始变量,导致意外行为。

go
var data interface{} = "great"
// 错误示例
if data, ok := data.(int); ok {
    fmt.Println("[is an int], data:", data)
} else {
    // 这里的 data 是 int 类型的零值 0,而不是原始的 "great"
    fmt.Println("[not an int], data:", data) // 输出: [not an int], data: 0
}

// 正确做法:使用不同的变量名
if res, ok := data.(int); ok {
    fmt.Println("[is an int], data:", res)
} else {
    // 这里的 data 仍然是原始的接口变量 "great"
    fmt.Println("[not an int], data:", data) // 输出: [not an int], data: great
}

类型选择 (Type Switch)

如果需要检查一个接口变量可能是多种类型中的哪一种,并根据其类型执行不同操作,使用类型选择 switch 语句会比一连串的 if-else 更清晰和高效。

语法: switch v := i.(type) { ... }

  • i 必须是接口类型。
  • v 在每个 case 分支中会是对应的具体类型。
  • type 是关键字。
go
package main

import "fmt"

func checkType(i interface{}) {
    switch v := i.(type) { // v 在每个 case 块中是该 case 对应的类型
    case int:
        fmt.Printf("类型是 int, 值是 %d, v 的类型是 %T\n", v, v)
    case string:
        fmt.Printf("类型是 string, 值是 '%s', v 的类型是 %T\n", v, v)
    case bool:
        fmt.Printf("类型是 bool, 值是 %t, v 的类型是 %T\n", v, v)
    case nil:
        fmt.Printf("值是 nil\n")
    default: // 可选
        fmt.Printf("未知的类型: %T, 值为: %v\n", v, v)
    }
}

func main() {
    checkType(42)
    checkType("Go语言")
    checkType(true)
    checkType(3.14) // 会进入 default
    var x interface{}
    checkType(x) // x 是 nil 接口值
}

// 输出:
// 类型是 int, 值是 42, v 的类型是 int
// 类型是 string, 值是 'Go语言', v 的类型是 string
// 类型是 bool, 值是 true, v 的类型是 bool
// 未知的类型: float64, 值为: 3.14
// 值是 nil

类型别名

类型别名 (Type Alias) vs. 定义新类型 (New Type): Go 1.9 引入了类型别名,在此之前只有定义新类型。

  • 类型别名 (Type Alias): type NewName = OldName

    • NewNameOldName 是完全相同的类型,只是名字不同。它们可以互换使用。
    • 它们可以互相赋值,不需要显式转换。
    • 类型别名主要用于代码重构或逐步迁移代码,例如,当一个类型从一个包移动到另一个包时,可以在旧位置创建一个指向新位置类型的别名。
    • 内置的 byteuint8 的别名,runeint32 的别名。
  • 定义新类型 (New Type / Type Definition): type NewName OldName

    • NewName 是一个全新的、独立的类型,尽管其底层类型 (underlying type) 是 OldName
    • NewNameOldName 之间不能直接赋值或比较(除非是无类型常量),需要显式转换。
    • 这种方式允许你为新类型定义自己的方法集,即使底层类型相同。这是定义领域特定类型的主要方式。
go
package main

import "fmt"

// 类型别名
type MyString = string      // MyString 和 string 是同一类型
type UserID = int64       // UserID 和 int64 是同一类型

// 定义新类型
type Celsius float64    // Celsius 是一个新类型,底层是 float64
type Fahrenheit float64 // Fahrenheit 是一个新类型,底层是 float64

// 为新类型定义方法
func (c Celsius) ToFahrenheit() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

func (f Fahrenheit) ToCelsius() Celsius {
    return Celsius((f - 32) * 5 / 9)
}

func main() {
    // 类型别名
    var s1 string = "hello"
    var s2 MyString = s1 // 直接赋值,因为 MyString IS string
    fmt.Printf("s1 type: %T, s2 type: %T, s1 == s2: %t\n", s1, s2, s1 == s2)

    var id1 int64 = 101
    var id2 UserID = id1 // 直接赋值
    fmt.Printf("id1 type: %T, id2 type: %T, id1 == id2: %t\n", id1, id2, id1 == id2)

    // 定义新类型
    var tempC Celsius = 25.0
    var tempF Fahrenheit = 77.0

    // tempC = tempF // 编译错误: cannot use tempF (type Fahrenheit) as type Celsius in assignment
    // 需要显式转换
    convertedC := Celsius(tempF) // 只是转换底层类型,不调用 ToCelsius
    fmt.Printf("%.1f F is %.1f C (direct conversion)\n", tempF, convertedC) // 注意:这里只是底层类型转换,数值可能不是期望的温度转换

    // 使用方法进行有意义的转换
    fmt.Printf("%.1f C is %.1f F (using method)\n", tempC, tempC.ToFahrenheit())
    fmt.Printf("%.1f F is %.1f C (using method)\n", tempF, tempF.ToCelsius())
}

// 输出:
// s1 type: string, s2 type: string, s1 == s2: true
// id1 type: int64, id2 type: int64, id1 == id2: true
// 77.0 F is 77.0 C (direct conversion)
// 25.0 C is 77.0 F (using method)
// 77.0 F is 25.0 C (using method)

Common Pitfall: 定义新类型不继承方法 从一个非接口类型(如 sync.Mutex)定义一个新类型时,新类型不会继承原有类型的方法。

go
type myMutex sync.Mutex

func main() {
    var mtx myMutex
    // mtx.Lock() // 编译错误: mtx.Lock undefined (type myMutex has no field or method Lock)
}

Best Practice: 如果你需要扩展一个类型并使用其原有方法,应该使用嵌入 (embedding) 的方式,将原类型作为匿名字段嵌入到你的新 struct 中。

go
type MyLocker struct {
    sync.Mutex // 嵌入Mutex,MyLocker自动获得Lock和Unlock方法
}

func main() {
    var locker MyLocker
    locker.Lock()   // 正确
    locker.Unlock() // 正确
}

Go常见运算符及优先级

在 Go 语言(Golang)中,运算符是用于对变量和常量执行操作的符号。Go 提供了丰富的运算符集合,包括算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符等。

了解更过运算符相关内容: 查阅 Go 官方文档

算术运算符

运算符 描述 示例
+ 加法 a + b
- 减法 a - b
* 乘法 a * b
/ 除法 a / b
% 取模 a % b
++ 自增(后缀) a++
-- 自减(后缀) a–

✅ 示例代码

go
func main() {
    var a int = 20
    var b int = 10

    fmt.Println("a + b =", a + b)
    fmt.Println("a - b =", a - b)
    fmt.Println("a * b =", a * b)
    fmt.Println("a / b =", a / b)
    fmt.Println("a % b =", a % b)
}

Note: Go 不支持前缀 ++a--a,且 a++a-- 是语句,不是表达式,不能用于赋值,如 b := a++ 是非法的。

Common Pitfall: ++-- 只是语句 : 在Go中,不能写 j := i++if i++ > 0。自增和自减只能作为独立的语句使用,这简化了代码,避免了潜在的求值顺序问题。

go
i := 0
i++ // 正确
// j := i++ // 编译错误: syntax error: unexpected ++, expecting }

比较运算符

运算符 描述
== 等于
!= 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于

✅ 示例代码

go
package main

import "fmt"

func main() {
    var a int = 20
    var b int = 10

    // 关系运算符
    result = (a == b)
    fmt.Printf("a == b -> %t\n", result)

    result = (a > b)
    fmt.Printf("a > b -> %t\n", result)
}

逻辑运算符

运算符 描述 示例
&& 逻辑与 (a > b) && (c < d)
|| 逻辑或 (a > b)
! 逻辑非 !(a > b)

✅ 示例代码

go
package main

import "fmt"

func main() {
    // 逻辑运算符
    var x, y bool = true, false
    fmt.Printf("x && y -> %t\n", x && y)
    fmt.Printf("x || y -> %t\n", x || y)
    fmt.Printf("!x -> %t\n", !x)
}

位运算符

运算符 描述
& 按位与
| 按位或
^ 按位异或 / 按位取反 (一元)
&^ 按位清除(a &^ b = a & (~b))
<< 左移
>> 右移

✅ 示例代码

go
package main

import "fmt"

func main() {
    var a int = 20
    var b int = 10

    // 位运算符
    fmt.Printf("a & b = %d\n", a & b)   // AND
    fmt.Printf("a | b = %d\n", a | b)   // OR
    fmt.Printf("a ^ b = %d\n", a ^ b)   // XOR
    fmt.Printf("a << 2 = %d\n", a << 2) // Left shift
    fmt.Printf("a >> 2 = %d\n", a >> 2) // Right shift
}

Note: 按位取反 Go 中没有专门的按位取反运算符 ~。按位取反 ^x (一元异或) 被用来实现这个功能。^x 等同于 x XOR m,其中 m 是所有位都为1的掩码。


赋值运算符

运算符 描述
= 赋值
+= 加后赋值
-= 减后赋值
*= 乘后赋值
/= 除后赋值
%= 取模后赋值
<<= 左移后赋值
>>= 右移后赋值
&= 按位与后赋值
|= 按位或后赋值
^= 异或后赋值

✅ 示例代码

go
package main

import "fmt"

func main() {
    var a int = 20
    var b int = 10

    // 赋值运算符
    c = a
    fmt.Println("c = a -> c =", c)

    c += a
    fmt.Println("c += a -> c =", c)

    c <<= 2
    fmt.Println("c <<= 2 -> c =", c)
}

其他运算符

运算符 描述
& 获取地址
* 指针取值
<- 向通道发送或从通道接收数据

Go运算符优先级

以下是 Go 中运算符的优先级顺序,数字越大表示优先级越高。

优先级 运算符 描述
5 * / % << >> & &^ 乘法、除法、取模、位移、按位与、按位清除
4 + - | ^ 加法、减法、按位或、按位异或
3 == != < <= > >= 比较运算符
2 && 逻辑与
1 || 逻辑或

注意: 所有一元运算符(如 * (取值), & (取址), <- (接收))拥有最高优先级。

Common Pitfall: 运算符优先级与其他语言的差异 Go 的运算符优先级与 C/C++/Java 等语言有所不同。例如,位移运算符 (<<, >>) 的优先级低于算术运算符 (+, -)。

go
// 在 C++ 中: (2 + 2) << 1  -> 4 << 1 -> 8
// 在 Go 中: 2 + (2 << 1) -> 2 + 4 -> 6
fmt.Println(2 + 2 << 1) // 输出: 6

Best Practice: 当不确定优先级时,总是使用括号 () 来明确意图,这能极大地提高代码的可读性和正确性。