一,函数
1.1 函数的基本结构
函数声明
Go 语言中函数的通用格式:
func 函数名(参数列表) (返回值列表) {
// 函数体
}
示例:
func add(a int, b int) int {
return a + b
}
关键点
func是声明函数的关键字。- 函数名遵循驼峰命名法(例如:
calculateSum)。 - 参数和返回值需要明确类型(Go 是静态类型语言),如果不需要返回值就不用写。
1.2 参数
形式参数(形参)
-
定义函数时声明的参数。
-
可以指定参数类型,多个相同类型的参数可简写:
func multiply(x, y int) int { // x 和 y 都是 int 类型 return x * y }
实际参数(实参)
-
调用函数时传递的具体值。
result := multiply(3, 4) // 3 和 4 是实参
不定参数(Variadic Parameters)
-
用
...表示接受任意数量的参数,本质上是一个切片。func sum(numbers ...int) int { total := 0 for _, num := range numbers { total += num } return total } // 调用 fmt.Println(sum(1, 2, 3)) // 输出 6
1.3 返回值
单返回值
-
func isEven(n int) bool { return n % 2 == 0 }
多返回值(Go 的特色)
-
常用于返回结果和错误信息。
func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } // 调用 result, err := divide(10, 2) if err != nil { log.Fatal(err) }
命名返回值(Named Return Values)
-
返回值可以在函数签名中命名,直接作为变量使用。
-
return语句可省略返回值变量名。func rectangleProps(length, width float64) (area, perimeter float64) { area = length * width perimeter = 2 * (length + width) return // 等价于 return area, perimeter } func main() { // 调用 area, perimeter := rectangleProps(10, 20) fmt.Println(area, perimeter) }
1.4 函数调用
基本调用
res := add(2, 3) // 返回 5
忽略返回值
-
用
_忽略不需要的返回值:_, err := someFunction()
1.5 高阶函数特性
函数可以:
- 被赋值给变量
- 被作为参数传递
- 被作为返回值返回
1.5.1 函数作为参数
package main
import "fmt"
//解释一下:operation是变量名,func(int,int) int是这个函数的入参和返回值类型,最后一个int是外部applyOperation函数的返回值类型
func applyOperation(a, b int, operation func(int, int) int) int {
return operation(a, b)
}
func main() {
// 调用
result := applyOperation(5, 3, func(x, y int) int { return x * y })
fmt.Println(result)
}
在 java 中这个函数式接口的有相同效果
1.5.2 函数作为返回值
//这个函数返回了一个函数func(int) int
func createMultiplier(factor int) func(int) int {
return func(x int) int {
return x * factor
}
}
func main() {
// 使用
double := createMultiplier(2)
fmt.Println(double(5)) // 输出 10
}
1.5.3 函数作为字段
语法:
// 声明函数类型
type HandlerFunc func(int, string) error
// 在结构体中使用函数类型
type Processor struct {
Name string
Process HandlerFunc // 函数作为字段
}
举例
//作为Go结构中的字段
package main
import "fmt"
// Finalsalary函数类型
type Finalsalary func(int, int) int
//创建结构
type Author struct {
name string
language string
Marticles int
Pay int
//函数作为字段
salary Finalsalary
}
func main() {
// 初始化字段结构
result := Author{
name: "Sonia",
language: "Java",
Marticles: 120,
Pay: 500,
salary: func(Ma int, pay int) int {
return Ma * pay
},
}
fmt.Println("作者姓名: ", result.name)
fmt.Println("语言: ", result.language)
fmt.Println("五月份发表的文章总数: ", result.Marticles)
fmt.Println("每篇报酬: ", result.Pay)
fmt.Println("总工资: ", result.salary(result.Marticles, result.Pay))
}
直接定义匿名函数类型
-
语法
type Server struct { // 直接使用匿名函数类型 OnConnect func(net.Conn) OnError func(error) } -
举例
//使用匿名函数作为Go结构中的一个字段 package main import "fmt" //创建结构 type Author struct { name string language string Tarticles int Particles int Pending func(int, int) int } func main() { //初始化结构字段 result := Author{ name: "Sonia", language: "Java", Tarticles: 340, Particles: 259, Pending: func(Ta int, Pa int) int { return Ta - Pa }, } fmt.Println("作者姓名: ", result.name) fmt.Println("语言: ", result.language) fmt.Println("文章总数: ", result.Tarticles) fmt.Println("发表文章总数: ", result.Particles) fmt.Println("待处理文章: ", result.Pending(result.Tarticles, result.Particles)) }
1.5.4 匿名函数(Lambda)
没有名字的函数,直接定义并使用。
func main() {
square := func(x int) int {
return x * x
}
fmt.Println(square(4)) // 16
}
1.5.5 闭包(Closure)
闭包的定义
- 闭包是一个 函数值(Function Value),它引用了其函数体之外的变量。这个函数可以访问这些变量,并“记住”这些变量的状态,即使外部函数已经执行完毕。闭包的核心是:函数 + 它引用的外部变量环境。
- 用生活场景类比:闭包就像一个随身携带背包的函数,背包里装的是它需要用到的外部变量。
- 闭包的本质是函数能够访问并记住其词法作用域外的变量
- 换句话说,当一个函数引用了它外层函数中的变量,即使外层函数已经执行完返回了,这些变量依然会“保活”,这个函数就成了一个“闭包”。
闭包的特性
- 捕获外部变量:闭包函数可以访问定义它的作用域中的变量。
- 变量生命周期延长:被闭包引用的变量不会随外部函数执行结束而销毁,而是会一直存在,直到闭包不再被使用。
代码示例
-
最简单的闭包
func counter() func() int { count := 0 return func() int { count++ return count } } //调用 c1 := counter() fmt.Println(c1()) // 1 fmt.Println(c1()) // 2 fmt.Println(c1()) // 3 c2 := counter() fmt.Println(c2()) // 1 (新的独立闭包)
重点理解:
count是counter()函数里的局部变量;- 返回的匿名函数
func() int { count++; return count }引用了count; - 所以即使
counter()函数已经结束,它的局部变量count并没有消失; - 每次调用
c1()都在“自己那份”count上加一。
原本方法里面的变量是在栈里面的,但因为被内部函数引用了,Go 自动将其放到堆里面持久化,后续就相当于指针
核心突破:作用域的延伸
func normalFunc() {
x := 10 // 局部变量
// 函数结束时 x 被销毁
}
func closureFunc() func() {
y := 20 // 被闭包捕获的变量
return func() {
y++ // 神奇之处:函数返回后还能修改 y!
fmt.Println(y)
}
}
1.5.6 回调函数
回调函数是作为参数传入另一个函数,由它在某个时机“调用回来”的函数。
你可以理解成:
“我把一个函数交给你,当你需要的时候,叫我。”
举个简单例子:
func doSomething(callback func(string)) {
fmt.Println("准备处理一些事情...")
callback("处理完了")
}
调用它:
doSomething(func(msg string) {
fmt.Println("回调函数被调用了:", msg)
})
输出:
准备处理一些事情...
回调函数被调用了:处理完了
1.6 函数的值传递
Golang 支持两种将参数传递给函数的方式,即按值传递或按值调用以及按引用传递或按引用传递。默认情况下,Golang 使用按值调用的方式将参数传递给函数。
1.6.1 按值调用
在此参数传递中,实际参数的值将复制到函数的形式参数中,并且两种类型的参数将存储在不同的存储位置中。因此,在函数内部进行的任何更改都不会反映在调用者的实际参数中。
** 示例 1:** 在下面的程序中,可以看到 Z 的值不能通过 Modify 函数修改。
package main
import "fmt"
// 函数修改值
func modify(Z int) {
Z = 70
}
func main() {
var Z int = 10
fmt.Printf("函数调用前,Z的值为 = %d", Z)
//按值调用
modify(Z)
fmt.Printf("\n函数调用后,Z的值为 = %d", Z)
}
输出:
函数调用前,Z的值为 = 10
函数调用后,Z的值为 = 10
1.6.2 按引用调用
在这里,将使Pointers(指针) 的概念。
- 引用运算符
*用于访问地址中的值。 - 地址运算符
&用于获取任何数据类型的变量的地址。
实际参数和形式参数都指向相同的位置,因此在函数内部所做的任何更改实际上都会反映在调用者的实际参数中。
示例:在函数调用中,我们传递变量的地址,并使用引用运算符修改值。因此,在函数即 *Modify 之后,将找到更新后的值。
package main
import "fmt"
// 修改值的函数
func modifydata(Z *int) {
*Z = 70
}
func main() {
var Zz int = 10
fmt.Printf("函数调用前,Zz的值为 = %d", Zz)
//通过引用调用传递变量Z地址
modifydata(&Zz)
fmt.Printf("\n函数调用后,Zz的值为 = %d", Zz)
}
输出
函数调用前,Zz的值为 = 10
函数调用后,Zz的值为 = 70
1.7 defer关键字
defer 是 Go 语言中一个非常实用的关键字,用于延迟执行某些操作。它的核心逻辑是 “延迟执行,但确保执行”,尤其在处理资源释放(如关闭文件、解锁、数据库连接回收)和错误处理时非常有用。
基本行为
defer会将函数或语句的执行推迟到 当前函数返回之前。- 多个
defer会按 后进先出(LIFO) 的顺序执行。
示例:
package main
import "fmt"
func f1() {
fmt.Println("f1")
}
func f2() {
fmt.Println("f2")
}
func f3() {
fmt.Println("f3")
}
func main() {
defer f1()
defer f2()
defer f3()
}
//输出f3,f2,f1
1.8 main和init函数
main 函数
在 Go 语言中,main包是一个特殊的软件包,与可执行程序一起使用,并且该 package 包含 *main()函数。main() 函数是一种特殊类型的函数,它是可执行程序的入口点。它不带任何参数也不返回任何内容。由于可以自动调用main()函数,因此无需显式调用main()函数,并且每个可执行程序必须包含一个 package main 和main()* 函数。
示例
//主包的声明
package main
//导入包
import (
"fmt"
func main() {
fmt.PrintLn("hello World")
}
init 函数
- init()函数就像main函数一样**,不带任何参数也不返回任何东西。** 每个包中都存在此函数,并且在初始化包时将调用此函数。
- 该函数是隐式声明的,因此不能从任何地方引用它,并且可以在同一程序中创建多个init()函数,并且它们将按照创建顺序执行。
- 可以在程序中的任何位置创建init()函数,并且它们以词汇文件名顺序(字母顺序)调用。
- 允许在init()函数中放置语句,但始终记住要在main()函数调用之前执行init()函数,因此它不依赖于main()函数。
- init()函数的主要目的是初始化无法在全局上下文中初始化的全局变量。
package main
import "fmt"
//多个init()函数
func init() {
fmt.Println("init 1")
}
func init() {
fmt.Println("init 2")
}
func main() {
fmt.Println("main")
}
输出:
init 1
init 2
main
二,结构体
Golang 中的结构 (struct) 是一种用户定义的类型,允许将可能不同类型的项分组 / 组合成单个类型。任何现实世界中拥有一组属性 / 字段的实体都可以表示为结构。这个概念通常与面向对象编程中的类进行比较。它可以被称为不支持继承但支持组合的轻量级类。
例如,一个地址具有name,street,city,state,Pincode。如下所示,将这三个属性组合为一个结构Address是有意义的。
2.1 声明结构
举例:
type Address struct {
name string
street string
city string
state string
Pincode int
}
在上面,type关键字引入了一个新类型。其后是类型的名称(Address)和关键字 *struct,* 以说明我们正在定义结构。该结构包含花括号内各个字段的列表。每个字段都有一个名称和类型。
** 注意:** 我们还可以通过组合相同类型的各个字段来使它们紧凑,如下例所示:
type Address struct {
name, street, city, state string
Pincode int
}
定义一个结构体变量
var a Address
上面的代码创建一个Address类型的变量,默认情况下将其设置为零。对于结构,零表示所有字段均设置为其对应的零值。因此,字段name,street,city,state都设置为“”,而Pincode设置为 0。
2.2 初始化结构体
字面量初始化:
p1 := Person{Name: "Alice", Age: 30} // 指定字段名,k:v结构,p1是 Person类型
p2 := Person{"Bob", 25} // 按字段顺序(需全部赋值),p2是 Person类型
new 函数:返回指针
p3 := new(Person) // p3 是 *Person 类型
p3.Name = "Charlie"
取地址初始化:
p4 := &Person{Name: "Dave", Age: 40} // 返回 *Person 指针
关键区别总结
| 特性 | p1,p2(值类型) | p4(指针+显式初始化) | p3(指针+零值初始化) |
|---|---|---|---|
| 类型 | Person | *Person | *Person |
| 字段初始化 | 显式赋值 | 显式赋值 | 零值(需后续赋值) |
| 内存行为 | 直接存储结构体数据 | 存储指向初始化数据的指针 | 存储指向零值数据的指针 |
| 复制开销 | 复制整个结构体(可能昂贵) | 仅复制指针(高效) | 仅复制指针(高效) |
| 修改影响范围 | 仅影响当前副本 | 影响所有指向该实例的指针 | 影响所有指向该实例的指针 |
2.3 访问与修改字段
通过 . 操作符访问或修改字段:
fmt.Println(p1.Name) // 输出: Alice
p1.Age = 31 // 修改字段值
2.4 结构体的比较
如果所有字段都是可比较类型(如基本类型),则结构体可比较。
- 包含不可比较字段(如切片、map)时,编译报错
p1 := Person{"Alice", 30}
p2 := Person{"Alice", 30}
fmt.Println(p1 == p2) // 输出: true
2.5 嵌套结构体
Go 语言中的嵌套结构体是一种通过组合实现复杂数据结构的常用方式,允许一个结构体包含另一个结构体作为其字段。这种设计遵循组合优于继承的原则,提供灵活的数据建模能力。
定义与初始化
// 嵌套结构体
package main
import "fmt"
// 创建结构体
type Author struct {
name string
branch string
year int
}
// 创建嵌套结构体
//写法1:具体名嵌套
type HR struct {
id int
//字段结构
details Author
}
//写法2:匿名嵌套
type HR2 struct{
id int
Author
}
func main() {
// 初始化结构体字段
result := HR{
id: 10,
details: Author{"Sona", "ECE", 2013},
}
// 嵌套结构体
resul2t := HR2{
id: 10,
Author: Author{"Sona", "ECE", 2013},
}
//打印输出值
fmt.Println("\n作者的详细信息")
fmt.Println(result2.name)// 直接访问!仿佛HR2里面本身就有这个字段!这个就是模拟继承
}
//结果:作者的详细信息
// {10 {Sona ECE 2013}}
具体名嵌套 vs 匿名嵌套
| 特性 | 具名嵌套 (HR) | 匿名嵌套 (HR2) |
|---|---|---|
| 字段定义 | details Author | Author |
| 访问内嵌字段 | hr.details.Name | hr2.Name |
| 字段冲突 | 天然避免(需显式路径) | 可能发生(需类型名限定) |
| 方法提升 | 无 | 有(可直接调用) |
| 设计意图 | 组合(强调包含关系) | 模拟继承(行为复用) |
2.6 匿名结构
在 Go 语言中,允许创建匿名结构。匿名结构是不包含名称的结构。当要创建一次性可用结构时,它很有用。
语法
// 定义并初始化匿名结构体
variable := struct {
Field1 Type1
Field2 Type2
// ...
}{
Field1: Value1,
Field2: Value2,
}
// 示例
person := struct {
Name string
Age int
}{
Name: "Alice",
Age: 30,
}
核心特点
-
无类型名称
没有通过type关键字声明,直接通过结构体字面量定义。 -
即定义即用
定义和初始化在同一个表达式中完成。 -
作用域受限
只能在当前作用域使用,无法在其他地方复用。 -
类型唯一性
即使两个匿名结构体字段完全相同,也被视为不同类型:a := struct{ X int }{1} b := struct{ X int }{2} // a = b // 编译错误:类型不匹配
与命名结构体的对比
| 特性 | 匿名结构体 | 命名结构体 |
|---|---|---|
| 类型名称 | 无 | 有(通过 type 声明) |
| 可复用性 | 仅当前作用域 | 整个包/项目 |
| 类型标识 | 结构相同也被视为不同类型 | 类型名称决定类型 |
| 适合场景 | 一次性使用、局部数据封装 | 重要数据结构、API边界定义 |
| 可序列化 | 支持(同命名结构体) | 支持 |
| 方法定义 | ❌ 不能定义方法 | ✅ 可以定义方法 |
2.7 匿名字段
在 Go 结构中,允许创建匿名字段。匿名字段是那些不包含任何名称的字段,你只需要提到字段的类型,然后 Go 就会自动使用该类型作为字段的名称。
语法
type struct_name struct{
int
bool
float64
}
重要事项:
-
在结构中,不允许创建两个或多个相同类型的字段,如下所示:
type student struct{ int int }如果尝试这样做,则编译器将抛出错误。
-
允许将匿名字段与命名字段组合,如下所示:
type student struct{ name int price int string } -
示例
package main import "fmt" //创建一个结构匿名字段 type student struct { int string float64 } // Main function func main() { // 将值分配给匿名,学生结构的字段 value := student{123, "Bud", 8900.23} fmt.Println("入学人数 : ", value.int) fmt.Println("学生姓名 : ", value.string) fmt.Println("套餐价格 : ", value.float64) // 输出:入学人数 : 123 学生姓名 : Bud 套餐价格 : 8900.23 }
2.8 结构体的比较
-
逐字段递归比较
结构体比较时会递归比较每个字段:
type Address struct { City, Zip string } type Employee struct { ID int address Address } e1 := Employee{1, Address{"Paris", "75000"}} e2 := Employee{1, Address{"Paris", "75000"}} e3 := Employee{1, Address{"Lyon", "69000"}} fmt.Println(e1 == e2) // true fmt.Println(e1 == e3) // false -
指针比较
指针比较的是内存地址,而不是指向的值:
type Node struct { Value int Next *Node } n1 := &Node{1, nil} n2 := &Node{1, nil} n3 := n1 fmt.Println(n1 == n2) // false - 不同地址 fmt.Println(n1 == n3) // true - 相同地址 -
空结构体的特殊性
空结构体始终相等
type Empty struct{} var e1, e2 Empty fmt.Println(e1 == e2) // true
2.9 new关键字
new(T) 会分配一块内存用于类型 T,并返回一个 指向类型 T 的指针 *T,内容是零值。
基本语法
p := new(int)
fmt.Println(*p) // 输出 0,int 的零值
*p = 42
fmt.Println(*p) // 输出 42
用 new 创建结构体指针
你已经知道了这一点:
type Person struct {
Name string
Age int
}
p := new(Person) // 返回 *Person,字段都为零值
p.Name = "Tom"
p.Age = 30
等价于:
p := &Person{} // 更常见、更 idiomatic 的方式
使用 new 创建基本类型的指针
p := new(bool) // *bool
fmt.Println(*p) // false(bool 的零值)
这在需要显式传递基本类型指针时很有用,比如:
func toggle(b *bool) {
*b = !*b
}
b := new(bool)
toggle(b)
fmt.Println(*b) // true
new 分配的变量是堆上还是栈上?
这是 Go 编译器决定的(逃逸分析),但从你的角度看:
new()返回的是一个 指针;- 你可以把这个指针在函数外使用,它不会被销毁;
- 所以可以认为
new通常分配的是堆上内存。
三,方法
在 Go 语言中,方法(Method)是一种与特定类型(Type)关联的函数,允许为自定义类型(包括结构体和非结构体类型)添加行为。方法的本质是在函数签名前增加一个接收者(Receiver),定义了该函数作用于哪种类型上。
在接收者参数的帮助下,该方法可以访问接收者的属性。
- 在这里,接收方可以是结构类型或非结构类型。
- 在代码中创建方法时,接收者和接收者类型必须出现在同一个包中。而且不允许创建一个方法,其中的接收者类型已经在另一个包中定义,包括像int、string等内建类型。
3.1 结构类型接收者
语法:
func(接收者 接收者类型) 方法名(参数列表)(返回值类型){
// Code
}
示例
package main
import "fmt"
//Author 结构体
type author struct {
name string
branch string
particles int
salary int
}
//接收者的方法
func (a author) show() {
fmt.Println("Author's Name: ", a.name)
fmt.Println("Branch Name: ", a.branch)
fmt.Println("Published articles: ", a.particles)
fmt.Println("Salary: ", a.salary)
}
func main() {
//初始化值
//Author结构体
res := author{
name: "Sona",
branch: "CSE",
particles: 203,
salary: 34000,
}
//调用方法
res.show()
}
3.2 非结构类型接收者
在 Go 语言中,只要类型和方法定义存在于同一包中,就可以使用非结构类型接收器创建方法。如果它们存在于 int,string 等不同的包中,则编译器将抛出错误,因为它们是在不同的包中定义的。
举例
package main
import "fmt"
//类型定义
type data int
//定义一个方法
//非结构类型的接收器
func (d1 data) multiply(d2 data) data {
return d1 * d2
}
/*
//如果您尝试运行此代码,
//然后编译器将抛出错误
func(d1 int)multiply(d2 int)int{
return d1 * d2
}
*/
func main() {
value1 := data(23)
value2 := data(20)
res := value1.multiply(value2)
fmt.Println("最终结果: ", res)
}
即如果要用非结构类型的接收者就需要用 type 封装一下
3.3 指针接收
在 Go 语言中,允许使用指针接收器创建方法。在指针接收器的帮助下,如果方法中所做的更改将反映在调用方中。
func (p *Person) Birthday() {
p.Age++ // 修改的是原始对象
}
调用时可以用值也可以用指针:
p := Person{Name: "Tom", Age: 20}
p.Birthday() // 自动转换为指针调用
(&p).Birthday() // 显式指针调用
3.4 值接收
func (p Person) SayName() {
fmt.Println(p.Name)
}
值接收者 vs 指针接收者
| 类型 | 语法 | 行为 | 是否修改原值 |
|---|---|---|---|
| 值接收者 | (t Type) | 操作接收者的副本 | ❌ 否 |
| 指针接收者 | (t *Type) | 操作接收者指向的内存(原始对象) | ✅ 是 |
3.5 方法继承和重写
通过结构体嵌入实现方法继承:
type Animal struct {
name string
}
func (a Animal) Speak() {
fmt.Println(a.name, "makes a sound")
}
type Dog struct {
Animal // 嵌入Animal(继承其字段和方法)
}
// 重写Speak方法
func (d Dog) Speak() {
fmt.Println(d.name, "barks!")
}
func main() {
d := Dog{Animal{"Buddy"}}
d.Speak() // 输出: Buddy barks!
}
Go语言函数与结构体
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。