函数(Function)
函数声明与调用对比
-
函数声明对比
go// 语法: // func 函数名([参数列表]) [返回值列表] { // // 函数体 // } // 示例1:基础形式(与Java类似) func add(a int, b int) int { return a + b } // 示例2:参数类型合并(a和b都是int类型) func add(a, b int) int { return a + b } // 示例3:多返回值(Go的核心特性!) func divide(a, b float64) (float64, error) { if b == 0.0 { return 0.0, errors.New("division by zero") } return a / b, nil } // 示例4:命名返回值 func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return // 称为"裸返回",自动返回x和y }
- 关键字
func
:使用func
关键字开头,而不是Java中的返回值类型。 - 类型后置:变量名在前,类型在后。这是Go的整体风格,与变量声明一致。
- 简洁的参数列表 :同类型的参数可以合并声明(
a, b int
)。 - 多返回值:这是Go与Java最显著的区别之一。函数可以返回多个值,通常用于返回结果和错误信息。
- 命名返回值 :可以为返回值命名,它们会被视为在函数顶部定义的变量。使用裸返回(
return
)时,会自动返回这些变量。 - 无异常声明 :Go没有
throws
关键字。错误通过普通的返回值来传递。
- 关键字
-
Go 的函数调用:直接且简单
go// 调用同一个包内的函数:直接调用 sum := add(5, 10) // 调用其他包的函数:pkg.FunctionName(args) result, err := math.Divide(10.0, 2.0) // 假设Divide在math包中 // 处理错误:通过判断返回值来处理,而非try-catch result, err := divide(10.0, 0.0) if err != nil { // 处理错误 fmt.Println("Error:", err) return } // 使用结果 fmt.Println("Result is", result) // 忽略某些返回值:使用空白标识符 _ file, _ := os.Open("filename.txt") // 忽略打开文件可能产生的错误(不推荐)
- 直接性:同一包内的函数直接调用,无需接收者。
- 多返回值处理 :调用返回多个值的函数时,必须用相同数量的变量来接收。可以使用
_
忽略不需要的值。 - 错误即值 :错误处理是普通的流程控制的一部分,通过
if err != nil
进行检查,而不是通过异常机制跳转。
多返回值
-
返回多个有效信息:直接返回
go// 函数直接返回两个值:(float64, float64) func divideWithRemainder(dividend, divisor int) (float64, float64) { quotient := float64(dividend) / float64(divisor) remainder := float64(dividend % divisor) return quotient, remainder // 直接返回两个值 } // 调用方:用两个变量来接收 q, r := divideWithRemainder(10, 3) fmt.Printf("商: %.2f, 余数: %.2f\n", q, r)
-
返回结果与错误状态:返回
(value, error)
goimport "errors" // 函数返回 (结果, 错误) func divide(dividend, divisor float64) (float64, error) { if divisor == 0.0 { // 错误路径:返回结果的零值和一個error return 0.0, errors.New("division by zero") } // 成功路径:返回结果和nil return dividend / divisor, nil } // 调用方:立即检查错误 result, err := divide(10.0, 0.0) if err != nil { // 处理错误:打印日志、返回错误、重试等... fmt.Println("Error:", err) return // 通常在这里返回,让错误向上传播 } // 如果err为nil,安全地使用result fmt.Println("Result is", result)
错误处理:Go的(value, error)
模式
-
语法和结构
go// 1. 返回错误:函数将 error 作为最后一个返回值。 // error 是一个内置接口,任何实现了 Error() string 方法的类型都可以作为错误。 import ( "errors" "os" ) func readFile(path string) (string, error) { file, err := os.Open(path) if err != nil { // 错误路径:返回结果的零值和一个错误。 // errors.New 是一个常用的创建简单错误的方法。 return "", errors.New("file not found: " + path) // 更常用的方式是直接返回底层操作产生的错误: return "", err } defer file.Close() // 确保文件被关闭,类似于 finally 的作用 // ... 读取文件内容 return content, nil // 成功路径:返回内容和一个 nil 错误 } // 2. 检查与处理错误:调用方立即使用 if 语句检查错误。 func processFile() { content, err := readFile("missing.txt") if err != nil { // 立即处理错误。这是Go代码中最常见的代码片段。 fmt.Fprintf(os.Stderr, "Failed to process file: %v\n", err) return // 通常,函数在处理错误后直接返回,将错误传递给它的调用者。 } // 如果 err == nil,说明成功,可以安全地使用 content fmt.Println(string(content)) }
-
性能零开销:错误处理就是普通的变量赋值和比较,效率极高,适用于任何频繁操作。
-
完全显式 :错误处理就在函数调用之后,一目了然。调用者无法忽视错误(除非故意用
_
忽略),所有错误路径都清晰可见。 -
控制流简单:程序始终保持线性的执行流程,没有跳转,更容易理解和维护。
-
简单性 :整个错误系统只有一个简单的
error
接口,没有复杂的异常类型体系。
匿名函数与Lambda表达式对比
-
核心概念与语法
- 语法 :
func(parameters) (return_types) { body }
- 本质 :它是一个闭包(Closure),可以捕获其所在作用域内的任何变量。
- 语法 :
-
语法和结构
go// 直接声明并调用一个匿名函数 func() { fmt.Println("Hello from immediate anonymous function") }() // 括号表示立即调用 // 将匿名函数赋值给一个变量 greet := func(name string) { fmt.Printf("Hello, %s\n", name) } greet("World") // 像调用普通函数一样调用 // 匿名函数作为参数传递 myPrint := func(f func(string), msg string) { f(msg) } myPrint(greet, "Gopher")
-
Go匿名函数的特点
- 独立性 :它是一个完整的函数,可以赋值给变量、作为参数传递、作为返回值,是一等公民。
- 完整的闭包 :可以捕获和修改其定义作用域内的任何变量,非常强大。
- 灵活性:不需要预定义的接口类型,可以定义任何签名的函数。
- 陷阱 :循环中捕获迭代变量是一个常见的陷阱(如上例中的
i
),需要通过参数传递或创建副本解决。
函数作为一等公民:Go的function type
vs Java的Functional Interface
-
机制详解 :可以使用
type
关键字为特定的函数签名定义一个类型,也可以直接使用函数签名。gopackage main import ( "fmt" "strings" ) // 1. 定义:使用`type`为函数签名创建一个自定义类型(推荐,更清晰) type StringProcessor func(string) int // 2. 使用:一个函数可以接受另一个函数作为参数 func processString(input string, processor StringProcessor) int { // 直接调用传入的函数! return processor(input) } // 也可以不自定义类型,直接使用函数签名 func processString2(input string, processor func(string) int) int { return processor(input) } func main() { text := "Hello" // 3. 传递:将一个符合签名的函数(这里是匿名函数)作为参数传递 result1 := processString(text, func(s string) int { return len(s) }) // 4. 也可以传递一个已定义的函数名(注意:没有括号) result2 := processString(text, len) // len 的签名是 func(string) int,符合要求 // 5. 函数可以赋值给变量 var myProcessor StringProcessor = strings.Count result3 := processString(text, myProcessor) // 计算 "Hello" 在 "Hello" 中出现的次数,结果是1 // 等价于: result3 := processString(text, func(s string) int { return strings.Count(s, "Hello") }) fmt.Println(result1, result2, result3) // 输出 5 5 1 }
-
函数即值 :你传递的就是函数本身 。变量
myProcessor
存储的是一个函数值,而不是一个实现了某个接口的对象。 -
直接调用 :要执行这个函数,直接使用括号
()
和参数即可,无需通过一个中间方法(如apply
)。 -
类型是签名 :变量
myProcessor
的类型是StringProcessor
或func(string) int
,这是一个描述函数输入和输出的类型。 -
灵活性:你可以为任何函数签名创建类型,无需预定义庞大的接口库。但Go标准库很少提供类似Java的高阶函数(如map, filter),通常需要自己实现。
面向对象编程(OOP)
核心概念颠覆:Go没有class
,只有struct
-
语法和结构
go// 一个Go的struct只负责定义数据 type Dog struct { // 1. 只有数据(字段) // 注意:字段名首字母大写表示"导出"(公共),小写表示"非导出"(私有) Name string Breed string age int // 私有字段,仅在包内可见 } // 2. 没有构造函数!通常用一个普通的工厂函数来模拟。 func NewDog(name, breed string) *Dog { return &Dog{ Name: name, Breed: breed, age: 0, // 可以初始化私有字段 } }
-
关键特性分析
- 内部不能定义方法 。所有方法都被定义在
struct
之外。 struct
没有继承 。Go语言完全没有extends
或implements
关键字。struct
就是一个内存布局的描述,它告诉你一块内存里按顺序放了什么数据。它非常轻量,接近C语言中的结构体。
- 内部不能定义方法 。所有方法都被定义在
-
方法(Methods)与接收者(Receiver)
go// 1. 为 Dog 类型定义一个方法 // (d Dog) 是"值接收者",它定义了此方法属于哪个类型。 // 在方法内部,`d` 是 Dog 的一个副本。 func (d Dog) Bark() { fmt.Println("Woof! My name is", d.Name) // 可以访问d的字段 } // 2. 通常我们会使用"指针接收者",这样才能修改结构体的数据 func (d *Dog) HaveBirthday() { d.age++ // 这样可以实际修改原结构体的age字段 fmt.Printf("%s is now %d years old.\n", d.Name, d.age) } // 3. 实现一个接口(例如一个Pet接口) type Pet interface { BeCute() } // 为 *Dog 实现 BeCute 方法,这样就实现了 Pet 接口 // (注意:Go的接口实现是隐式的,无需声明) func (d *Dog) BeCute() { fmt.Println(d.Name, "is being cute by wagging its tail.") } func main() { myDog := NewDog("Rex", "Shepherd") myDog.Bark() // 调用方法 myDog.HaveBirthday() // 修改内部状态 myDog.BeCute() // 传递给我们期望 Pet 接口的函数 PetCuteSession(myDog) } func PetCuteSession(p Pet) { p.BeCute() }
方法定义:Go的func (s Struct) method()
-
语法和结构
go// 数据 (结构体) type Circle struct { radius float64 } // 行为 (方法) - 定义在类型外部! // func (接收者变量 接收者类型) 方法名([参数列表]) [返回值列表] { ... } // 1. 值接收者 (Value Receiver) // (c Circle) 是接收者声明。`c`相当于Java中的`this`,但它是显式声明的。 func (c Circle) Area() float64 { return math.Pi * c.radius * c.radius } // 2. 指针接收者 (Pointer Receiver) - 更常见,用于修改数据 func (c *Circle) SetRadius(newRadius float64) { c.radius = newRadius // 这里可以修改原结构体的数据 } // 一个普通的函数,与任何类型无关 func PrintArea(c Circle) { fmt.Println(c.Area()) }
-
关键特性分析
-
位置 :方法被定义在结构体(或任何类型)的外部。它看起来就像一个普通函数,前面多了一个接收者声明。
-
显式的接收者 :
(c Circle)
或(c *Circle)
是显式声明的接收者。它定义了该方法属于哪种类型。c
是接收者变量名(通常很短,如1-2个字母),相当于Java的this
。Circle
或*Circle
是接收者类型,决定了方法是属于值类型还是指针类型。 -
访问控制 :Go没有
public
/private
关键字。方法的可见性由其名称的首字母大小写控制:Area()
(大写开头 ):表示方法是导出的(Public),可以被其他包访问。internalMethod()
(小写开头 ):表示方法是包内私有的(Private),只能在定义它的包内使用。 -
调用方式 :语法上与Java完全相同,通过变量.方法() 的形式调用。Go会自动处理值和指针的转换。
-
封装:Go的标识符首字母大小写
-
标识符规则
- 首字母大写 :表示导出(Exported) (相当于
public
)。可以被其他包导入并使用。 - 首字母小写 :表示未导出(Unexported) (相当于
package-private
或private
)。只能在定义它的当前包内使用。
- 首字母大写 :表示导出(Exported) (相当于
-
语法和结构
go// 在 package 'mypackage' 中 // MyStruct 是导出的(公共的),因为首字母大写。 // 其他包可以: var s mypackage.MyStruct type MyStruct struct { ExportedField int // 导出的字段(公共),其他包可以访问 unexportedField string // 未导出的字段(私有),仅限本包使用 } // NewMyStruct 是导出的函数(公共的),其他包可以调用。 func NewMyStruct() *MyStruct { return &MyStruct{} } // helperFunc 是未导出的函数(私有的),只能在 mypackage 包内调用。 func helperFunc() { // ... } // 为 MyStruct 定义方法 func (m *MyStruct) GetValue() int { // 方法名首字母大写,是导出的(公共的) return m.ExportedField } func (m *MyStruct) setValue(v int) { // 方法名首字母小写,是未导出的(私有的) m.unexportedField = "set" }
组合:Go使用组合(Embedding)替代继承
-
语法:嵌入
go// Go: 使用组合嵌入替代继承 type Animal struct { // 相当于"父类",但只是一个普通结构体 Name string } // 为 Animal 定义方法 func (a *Animal) Eat() { fmt.Println(a.Name, "is eating.") } // Dog "有一个" Animal (has-a),而不是 "是一个" Animal type Dog struct { Animal // 嵌入:没有字段名,只写类型。这是关键! Breed string } // 为 Dog 定义自己的方法 func (d *Dog) Bark() { fmt.Println(d.Name, "says: Woof!") // 可以直接访问嵌入类型的字段! } // 可以"重写"嵌入类型的方法 func (d *Dog) Eat() { d.Animal.Eat() // 可以选择性调用"父类"的方法 fmt.Println("... and it's dog food!") } func main() { myDog := Dog{ Animal: Animal{Name: "Rex"}, // 初始化嵌入的结构体 Breed: "Shepherd", } // 神奇之处:Dog 可以直接调用 Animal 的所有方法和字段! myDog.Eat() // 调用"重写"后的方法 myDog.Bark() // 调用自己的方法 // Go 也实现了多态,但通过接口(interface)而不是继承 var livingThing interface{ Eat() } = &myDog // 定义一个接口并赋值 livingThing.Eat() }
-
关键特性分析
-
自动提升(Promotion)
当嵌入一个类型(
Animal
)时,其所有字段和方法会自动**"提升"** 到外部类型(Dog
)的级别。myDog.Name
和myDog.Eat()
可以直接调用,仿佛这些字段和方法就是定义在Dog
本身一样。 -
模拟"重写"
可以在外部类型(
Dog
)上定义一个与内部类型(Animal
)同名的方法,这样就"覆盖"了内部类型的方法。仍然可以通过显式指定内部类型(
d.Animal.Eat()
)来调用被"覆盖"的方法,这比Java的super
更清晰、更灵活。 -
类型关系
Dog
和Animal
是两种完全不同的类型,没有继承关系。这意味着你不能直接将一个
Dog
赋值给一个Animal
变量(不像Java那样是自动的)。多态通过接口实现 :如果
Dog
实现了某个接口,那么它就可以被当作该接口类型使用。
-
多态:Go的interface
(非侵入式、鸭子类型)
-
语法和结构
go// 1. 定义一个接口(契约) // 只包含行为(方法),不包含任何数据。 type Speaker interface { Speak() // 接口方法 } // 2. 定义一些类型 // 注意:这些类型完全不知道 Speaker 接口的存在! type Dog struct { Name string } // 为 Dog 类型定义一个方法。 // 这个方法签名(无参数,无返回值)偶然地与 Speaker 接口的 Speak 方法匹配。 func (d Dog) Speak() { fmt.Printf("%s says: Woof!\n", d.Name) } type Human struct { Name string } // 为 Human 类型也定义一个签名相同的方法。 func (h Human) Speak() { fmt.Printf("%s says: Hello!\n", h.Name) } type Car struct { Model string } func (c Car) Drive() { // 这个方法叫 Drive,与 Speaker 接口无关 fmt.Println("Vroom!", c.Model) } // 3. 使用:多态 func makeItTalk(s Speaker) { // 参数是 Speaker 接口类型 s.Speak() // 调用接口方法 } func main() { dog := Dog{Name: "Rex"} human := Human{Name: "Alice"} car := Car{Model: "Tesla"} makeItTalk(dog) // ✅ Rex says: Woof! makeItTalk(human) // ✅ Alice says: Hello! // makeItTalk(car) // ❌ 编译错误!Car 没有实现 Speaker 接口(缺少 Speak 方法) // 接口实现是自动的,我们甚至可以即兴创建 var s Speaker s = dog // ✅ 因为 Dog 实现了 Speaker 所需的方法,所以可以赋值 s.Speak() }
-
关键特性分析
- 非侵入性 :类型无需关心接口,无需显式声明
implements
。接口和实现者之间没有直接的编译期依赖。 - 解耦极致 :你可以为已经存在的、来自其他包的类型定义新的接口,而无需修改它们的源代码。这极大地降低了耦合度。
- 鸭子类型:只关心行为(有什么方法),不关心身份(是什么类型)。
- 小而美 :鼓励定义很多小的、单一的接口(如
io.Reader
,io.Writer
),而不是庞大复杂的接口。
- 非侵入性 :类型无需关心接口,无需显式声明
构造函数:Go的工厂函数NewMyStruct()
-
语法和结构
gopackage user import ( "errors" "time" ) // 结构体定义(只有数据) type User struct { name string // 小写开头,包外不可访问(私有) Age int // 大写开头,包外可访问(公共) id int64 } // 1. 标准的工厂函数:函数名以 `New` 开头,返回一个 *User(指针) // 这样可以避免大结构体的值拷贝,并且能返回 nil。 func NewUser(name string, age int) (*User, error) { // 多返回一个 error 是常见模式 if name == "" { return nil, errors.New("name cannot be empty") // 可以在初始化时进行验证! } // 返回一个初始化好的 User 结构体的地址 return &User{ name: name, Age: age, id: time.Now().UnixNano(), // 可以在此处设置默认逻辑 }, nil } // 2. "构造函数"重载:通过不同的函数名来实现 func NewAnonymousUser() *User { return &User{ name: "Anonymous", id: -1, } } // 3. 可以返回接口,而不是具体类型,隐藏实现细节 type Speaker interface { Speak() string } // NewSpeaker 返回一个接口类型,调用者不知道底层具体是哪个结构体 func NewSpeaker() Speaker { return &User{name: "Echo"} // 这里返回的是 *User,但对外是 Speaker 接口 } // 为 User 实现 Speaker 接口的方法 func (u *User) Speak() string { return "Hello, my name is " + u.name } // 使用 func main() { // 使用内置的 new():只分配零值内存,极少使用 u1 := new(User) // u1 是 *User 类型,所有字段为其零值:{"", 0, 0} // 使用结构体字面量:直接初始化 u2 := &User{Name: "Bob", Age: 25} // 注意:如果字段是私有的(name),这里无法初始化! // 标准做法:调用工厂函数 u3, err := user.NewUser("Alice", 30) if err != nil { panic(err) } fmt.Println(u3.Age) // 访问公共字段 u4 := user.NewAnonymousUser() var s user.Speaker = user.NewSpeaker() s.Speak() }
-
关键特性分析
- 约定,非语法 :
NewXxx
只是一个命名约定,它不是Go语言的关键字或特殊语法。它就是一个返回结构体实例的普通函数。 - 可以有任何名字 :
New
,NewUser
,OpenFile
,CreateClient
等。 - 可以返回任何类型 :通常返回指针
*Struct
(避免拷贝,允许修改),但也可以返回值Struct
。 - 可以执行复杂逻辑 :可以在函数内进行参数验证 、分配ID 、设置默认值 、连接外部服务等。这是它比Java构造函数强大的地方。
- 可以返回错误 :这是Go方式最大的优势之一。如果初始化可能失败(如验证失败、网络错误),它可以返回
(obj, error)
,而Java构造函数无法抛出受检异常以外的错误。 - 可以隐藏实现:通过返回接口类型,而不是具体结构体类型,可以向调用者隐藏实现细节。
- 约定,非语法 :
集合类型
数组与切片:Go的Array
和更重要的Slice
-
Array语法和结构
go// 1. 声明和初始化 var goArray [5]int // 声明一个长度为5的int数组,元素初始化为0 names := [3]string{"Alice", "Bob", "Charlie"} // 声明并初始化 quickInit := [...]int{1, 2, 3, 4, 5} // 编译器推断长度,结果是 [5]int // 2. 访问和修改 goArray[0] = 10 firstElement := goArray[0] length := len(goArray) // 使用内置函数 len() 获取长度 // 3. 关键特性(与Java最大不同) // - **值类型(Value Type)**:这是最重要的区别! // 数组变量代表的是整个数组,而不是指向数组的指针(引用)。 // 赋值和传参会发生整个数组的拷贝。 a := [3]int{1, 2, 3} b := a // 这里是整个数组的完整拷贝!b 是 a 的一个副本。 b[0] = 100 // 修改 b 不会影响 a fmt.Println(a) // [1 2 3] fmt.Println(b) // [100 2 3] // 4. 缺点 // 因为它是值类型且长度固定,所以在函数间传递大数组开销很大,且无法动态扩容。 // 因此,在Go中,**数组直接使用的场景很少**。
-
Slice
go// 1. 创建切片(多种方式) // a) 基于数组创建(切片引用该数组) arr := [5]int{1, 2, 3, 4, 5} slice1 := arr[1:4] // slice1 = [2, 3, 4], len=3, cap=4 (从索引1开始到末尾) // b) 使用 make() 函数创建(同时创建底层数组) slice2 := make([]int, 3, 5) // 类型,长度(len),容量(cap) // slice2 = [0, 0, 0], len=3, cap=5 // c) 直接使用切片字面量(语法类似数组,但不指定长度) slice3 := []string{"a", "b", "c"} // 这是切片!不是数组! // slice3 = [a, b, c], len=3, cap=3 // 2. 操作切片 // - 访问和修改:与数组相同 slice3[0] = "A" // - 追加元素:使用内置 append() 函数,这是实现"动态"的关键! slice3 = append(slice3, "d") // 容量不足时,append 会自动扩容! // slice3 = [A, b, c, d], len=4, cap=6? (扩容策略通常是翻倍) // - 获取长度和容量 l := len(slice3) c := cap(slice3) // - 切片操作(创建新切片) newSlice := slice3[1:3] // [b, c] 新切片和原切片共享底层数组! newSlice[0] = "B" // 这会同时修改 slice3[1] 和 newSlice[0] fmt.Println(slice3) // [A, B, c, d] // 3. 关键特性 // - **引用类型**:切片本身是一个小的描述符(包含指针、len、cap),赋值和传参拷贝的是这个描述符,而不是底层数组。多个切片可以共享同一个底层数组。 // - **动态增长**:通过 `append()` 函数,切片可以在容量不足时自动扩容(分配新的更大的数组并拷贝数据)。 // - **高效"视图"**:切片操作(s[i:j])非常高效,因为它只是创建了一个新的切片描述符,而不拷贝底层数据。
- 指针(Pointer):指向底层数组的起始元素(不一定是数组头)。
- 长度(Length) :切片中当前有多少个元素(
len(s)
)。 - 容量(Capacity) :从切片起始位置到底层数组末尾的元素个数(
cap(s)
)。
映射:Go的map[keyType]valueType
-
语法和结构
gopackage main import "fmt" func main() { // 1. 初始化:使用 make() 函数或字面量初始化 // 语法:map[KeyType]ValueType // a) 使用 make userAges := make(map[string]int) // 创建一个map,key为string,value为int // b) 使用字面量(推荐初始化时直接赋值) userAges2 := map[string]int{ "Alice": 30, // 注意:每行末尾要有逗号 "Bob": 25, } // 2. 增删改查:使用类似数组的索引语法,非常直观 // - 添加/更新元素:使用 `map[key] = value` userAges["Alice"] = 30 userAges["Bob"] = 25 userAges["Alice"] = 31 // 更新 Alice 的值 // - 获取元素:使用 `value := map[key]` age := userAges["Alice"] fmt.Println(age) // 输出 31 // - 检查键是否存在:获取元素时可以接收第二个返回值(bool) age, exists := userAges["Charlie"] // 如果键不存在,exists 为 false,age 为值类型的零值 if exists { fmt.Println("Charlie's age is", age) } else { fmt.Println("Charlie does not exist in the map.") // 会执行这条 } // - 删除元素:使用内置的 delete() 函数 delete(userAges, "Bob") // - 获取大小:使用内置的 len() 函数 size := len(userAges) fmt.Println("Size:", size) // 3. 遍历:使用 `for range` 循环,语法极其简洁 for key, value := range userAges { fmt.Printf("%s is %d years old.\n", key, value) } // 4. 空值(nil)处理: // - 未初始化的map是nil,无法直接使用 var nilMap map[string]int // nilMap 是 nil // nilMap["key"] = "value" // 会导致运行时 panic! // - 值为零值:如果键不存在,`map[key]` 会返回值类型的零值(0, "", nil等) // 这比Java更明确,但也要结合 `exists` 检查来区分"零值"和"不存在"。 fmt.Println(userAges["UnknownKey"]) // 输出 0 (int的零值) }
-
关键特性分析
- 内置类型:是语言语法的一部分,不是标准库中的类。
- 语法操作 :使用类似数组的索引语法
[]
进行赋值和访问,更简洁。 - 多返回值 :通过
value, exists := map[key]
的双返回值模式完美解决了Java中"空值歧义"的问题。 - 内置函数支持 :使用
make()
创建,delete()
删除,len()
获取大小。 - 零值机制 :未初始化的map是
nil
,尝试写入会导致panic。读取不存在的键返回 value类型的零值。
迭代:Go的for-range
-
语法和结构
gopackage main import "fmt" func main() { // 1. 遍历切片(Slice)或数组(Array) - 最常用 names := []string{"Alice", "Bob", "Charlie"} // 语法:for index, value := range collection { ... } // 每次迭代返回两个值:索引和该索引处元素的副本 for index, name := range