Golang变量的使用
功能 | 说明 | 示例 |
---|---|---|
声明变量 | 使用 var 或 := |
var a int = 10 / b := 20 |
匿名变量 | 使用 _ 忽略变量 |
_ , b := getValues() |
命名风格 | 小写驼峰,导出用大写 | userName , MaxValue |
作用域 | 块级作用域 | { var x int } |
零值 | 未赋值时自动赋默认值 | var s string => "" |
变量声明与赋值
Go(Golang)是一门静态类型语言,变量在使用前必须声明。它对变量的声明和使用有明确的规范,语法简洁且安全。
1. 使用 var
声明变量(标准方式)
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 编译器可以根据赋值自动推断类型,所以你可以省略类型:
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)——最常用
使用 :=
可以同时声明并赋值变量,只能在函数内部使用:
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 中使用下划线 _
表示“丢弃”的值,常用于忽略不需要使用的返回值或变量。
示例:忽略函数返回值
package main
import "fmt"
func getValues() (int, int) {
return 100, 200
}
func main() {
_, b := getValues() // 忽略第一个返回值
fmt.Println("b =", b)
}
示例:忽略结构体字段遍历中的索引
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),即变量只在声明它的 {}
内部有效:
func main() {
if true {
insideIf := "I'm inside if"
fmt.Println(insideIf)
}
// 下面这行会报错:undefined: insideIf
// fmt.Println(insideIf)
}
Common Pitfall: 变量覆盖 (Shadowing) 在一个新的作用域(如
if
或for
块)中使用:=
可能会无意中创建一个同名的新变量,而不是修改外部作用域的变量。这是一种非常常见且难以发现的错误。govar 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。
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 |
结构体中每个字段的零值 |
示例代码:
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的行为差异
- 接口 (interface): 一个
nil
接口值 (var i interface{}
) 和一个持有nil
指针的接口值 (var p *int; i = p
) 是不相等的。前者类型和值都为nil
,后者类型为*int
,值为nil
。if i != nil
在后一种情况下为true
,是常见的错误源。- 切片 (slice): 对一个
nil
切片执行len()
,cap()
,for range
或append
操作都是安全的。- Map: 对一个
nil
map 执行len()
,for range
或读取操作是安全的(读取会返回零值)。但是,向一个nil
map 写入数据会立即引发panic
。
Golang数据类型及转换
Go语言(又称Golang)是一种静态类型语言,具有丰富的数据类型系统。
- Go 的基本数据类型 (Basic Types):布尔型、数字类型和字符串
- Go 的复合数据类型:数组、切片、Map
布尔型 (Boolean)
布尔型只有两个值:true
和 false
。默认值: false
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 |
无符号整型,足以存储一个指针的位 |
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位 | 双精度浮点数(默认) |
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 |
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编码的文本
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 对象,可以进行多次高效的替换。 |
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
在内存中都是连续的字节序列 -
都可以通过索引访问单个字节
-
可以相互转换:
gos := "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 |
计算 b 中 sep 出现的次数。 |
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 |
一个可变长度的字节缓冲区,提供了 Read 和 Write 方法,常用于高效构建字节序列。 |
示例代码 (bytes.Buffer
):
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 之间安全地传递数据,实现并发通信。
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
是要转换的值。
数字类型之间的转换
不同数字类型之间不能直接进行算术运算,必须先转换成相同类型。
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) |
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 操作都使用字节切片。
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
和 []byte
到 string
的转换会发生内存拷贝。对于高性能场景,需注意其开销。
类型断言与类型选择
类型断言 (Type Assertion) 与类型选择 (Type Switch)主要用于接口类型 interface{}
。当有一个接口类型的值,但想知道它底层存储的具体类型,并希望使用该具体类型的值时,就需要用到类型断言或类型选择。
类型断言
语法: value.(T)
这会尝试将接口值 value
转换为具体类型 T
。
- 如果
value
的底层动态类型确实是T
,转换成功,返回T
类型的值。 - 如果
value
的底层动态类型不是T
,程序会panic
(崩溃)。 - 如果
value
是nil
接口值,也会panic
。
为了安全地进行断言,可以使用“comma, ok”范式:v, ok := value.(T)
。
- 如果转换成功,
ok
为true
,v
是转换后的T
类型的值。 - 如果失败(类型不匹配或
value
为nil
),ok
为false
,v
是T
类型的零值,程序不会panic
。
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
且值为0
的data
,而不是原始的接口变量data
。这会覆盖原始变量,导致意外行为。govar 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
是关键字。
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
NewName
和OldName
是完全相同的类型,只是名字不同。它们可以互换使用。- 它们可以互相赋值,不需要显式转换。
- 类型别名主要用于代码重构或逐步迁移代码,例如,当一个类型从一个包移动到另一个包时,可以在旧位置创建一个指向新位置类型的别名。
- 内置的
byte
是uint8
的别名,rune
是int32
的别名。
-
定义新类型 (New Type / Type Definition):
type NewName OldName
NewName
是一个全新的、独立的类型,尽管其底层类型 (underlying type) 是OldName
。NewName
和OldName
之间不能直接赋值或比较(除非是无类型常量),需要显式转换。- 这种方式允许你为新类型定义自己的方法集,即使底层类型相同。这是定义领域特定类型的主要方式。
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
)定义一个新类型时,新类型不会继承原有类型的方法。gotype 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
中。gotype 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– |
✅ 示例代码
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
。自增和自减只能作为独立的语句使用,这简化了代码,避免了潜在的求值顺序问题。goi := 0 i++ // 正确 // j := i++ // 编译错误: syntax error: unexpected ++, expecting }
比较运算符
运算符 | 描述 |
---|---|
== |
等于 |
!= |
不等于 |
> |
大于 |
< |
小于 |
>= |
大于等于 |
<= |
小于等于 |
✅ 示例代码
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) |
✅ 示例代码
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)) |
<< |
左移 |
>> |
右移 |
✅ 示例代码
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的掩码。
赋值运算符
运算符 | 描述 |
---|---|
= |
赋值 |
+= |
加后赋值 |
-= |
减后赋值 |
*= |
乘后赋值 |
/= |
除后赋值 |
%= |
取模后赋值 |
<<= |
左移后赋值 |
>>= |
右移后赋值 |
&= |
按位与后赋值 |
|= |
按位或后赋值 |
^= |
异或后赋值 |
✅ 示例代码
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: 当不确定优先级时,总是使用括号
()
来明确意图,这能极大地提高代码的可读性和正确性。