Go语言进阶篇:如何深入理解函数、结构体、方法与接口?

2026-05-22 15:152阅读0评论SEO教程
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计3416个文字,预计阅读时间需要14分钟。

Go语言进阶篇:如何深入理解函数、结构体、方法与接口?

目录

1.函数

1.1 函数返回值 1.1.1 同一类型返回值 1.1.2 带变量名的返回值 1.1.3 函数中的参数传递 1.1.4 函数变量 1.2 匿名函数 1.2.1 无函数名函数 1.2.2 定义时调用匿名函数 1.2.3 将匿名函数赋值给变量

目录
  • 1. 函数
    • 1.1 函数返回值
      • 同一种类型返回值
      • 带变量名的返回值
      • 函数中的参数传递
      • 函数变量
    • 1.2 匿名函数——没有函数名字的函数
      • 在定义时调用匿名函数
      • 将匿名函数赋值给变量
      • 匿名函数用作回调函数
      • 可变参数——参数数量不固定的函数形式
    • 1.3 闭包
    • 1.4 defer语句
      • 处理运行时发生的错误
      • 1.5 宕机恢复(recover)——防止程序崩溃
  • 2. 结构体
    • 2.1 定义与给结构体赋值
  • 3. 方法
    • 结构体方法
    • 接收器
      • 指针接收器
      • 非指针类型接收器
  • 4. 接口
    • 声明接口
    • 实现接口

1. 函数

Go语言的函数属于“一等公民”(first-class),也就是说:

  • 函数本身可以作为值进行传递。
  • 支持匿名函数和闭包(closure)。
  • 函数可以满足接口。
1.1 函数返回值 同一种类型返回值

func typedTwoValues() (int, int) { return 1, 2 } a, b := typedTwoValues() fmt.Println(a, b) 带变量名的返回值

func named Ret Values() (a, b int) { a = 1 b = 2 return }

函数使用命名返回值时,可以在return中不填写返回值列表,如果填写也是可行的

函数中的参数传递

Go语言中传入和返回参数在调用和返回时都使用值传递,这里需要注意的是指针、切片和map等引用型对象指向的内容在参数传递中不会发生复制,而是将指针进行复制,类似于创建一次引用。

函数变量

在Go语言中,函数也是一种类型,可以和其他类型一样被保存在变量中

func fire() { fmt.Println("fire") } func main() { var f func() //将变量f声明为func()类型,此时f就被俗称为“回调函数”。此时f的值为nil。 f = fire f() } 1.2 匿名函数——没有函数名字的函数

Go语言支持匿名函数,即在需要使用函数时,再定义函数,匿名函数没有函数名,只有函数体,函数可以被作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式被传递

在定义时调用匿名函数

匿名函数可以在声明后调用,例如:

func(data int) { fmt.Println("hello", data) }(100) 将匿名函数赋值给变量

匿名函数体可以被赋值,例如:

// 将匿名函数体保存到f()中 f := func(data int) { fmt.Println("hello", data) } // 使用f()调用 f(100) 匿名函数用作回调函数

使用时再定义匿名函数,不使用先在被调用函数里面进行声明,这就是回调精髓

// 遍历切片的每个元素,通过给定函数进行元素访问 func visit(list []int, f func(int)) { for _, v := range list { f(v) } } func main() { // 使用匿名函数打印切片内容 visit([]int{1, 2, 3, 4}, func(v int) { fmt.Println(v) }) } 可变参数——参数数量不固定的函数形式

1.所有参数都是可变参数:fmt.Println

func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...) }

fmt.Println在使用时,传入的值类型不受限制,例如:

Go语言进阶篇:如何深入理解函数、结构体、方法与接口?

fmt.Println(5, "hello", &struct{ a int }{1}, true)

当可变参数为interface{}类型时,可以传入任何类型的值

2.部分参数是可变参数:fmt.Printf
fmt.Printf的第一个参数为参数列表,后面的参数是可变参数:

func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...) } ------------------------------------------------------ fmt.Printf("pure string\n") fmt.Printf("value: %v %f\n", true, math.Pi) 1.3 闭包

闭包可以理解成定义在函数内部的一个函数。本质上,闭包是函数内部和函数外部连接起来的桥梁。 简单来说,闭包=函数+引用环境

func main() { var f = add() fmt.Printf("f(10): %v\n", f(10)) fmt.Printf("f(20): %v\n", f(20)) // f(10): 10 // f(20): 30 } func add() func(int) int { var x int return func(y int) int { x += y return x } } 1.4 defer语句

defer语句将其后面跟随的语句进行延迟处理,被defer的语句按先进后出的方式执行(最先defer的语句最后执行,后被defer的语句先执行)。
特性:

  • 关键字defer用于注册延迟调用
  • 直到调用return之前才执行(故可用来作资源清理)
  • 多个defer语句,FILO方式执行
  • defer中的变量,在defer声明时就定义了

用途:

  • 关闭文件句柄
  • 锁资源释放
  • 数据库连接释放
处理运行时发生的错误

Go语言的错误处理思想及设计包含以下特征:
● 一个可能造成错误的函数,需要返回值中返回一个错误接口(error)。如果调用是成功的,错误接口将返回nil,否则返回错误。
● 在函数调用后需要检查错误,如果发生错误,进行必要的错误处理。

错误接口的定义格式
error是Go系统声明的接口类型,代码如下:

type error interface { Error() string // 返回错误的具体描述. }

所有符合Error() string格式的接口都能实现错误接口。

定义一个错误
在Go语言中,使用errors包进行错误的定义,格式如下:

var err = errors.New("this is an error")

错误字符串由于相对固定,一般在包作用域声明,应尽量减少在使用时直接使用errors.New返回。

宕机(panic)——程序终止运行
1 手动触发宕机
Go语言可以在程序中手动触发宕机,让程序崩溃,这样开发者可以及时地发现错误,同时减少可能的损失。
Go语言程序在宕机时,会将堆栈和goroutine信息输出到控制台,所以宕机也可以方便地知晓发生错误的位置。
package main
func main() {
panic("crash")
}

panic()的参数可以是任意类型,

当panic()触发的宕机发生时,panic()后面的代码将不会被运行,但是在panic()函数前面已经运行过的defer语句依然会在宕机发生时发生作用,

1.5 宕机恢复(recover)——防止程序崩溃

无论是代码运行错误由Runtime层抛出的panic崩溃,还是主动触发的panic崩溃,都可以配合defer和recover实现错误捕捉和恢复,让代码在发生崩溃后允许继续运行。

Go没有异常系统,其使用panic触发宕机类似于其他语言的抛出异常,那么recover的宕机恢复机制就对应try/catch机制。

panic和recover的关系:
● 有panic没recover,程序宕机。
● 有panic也有recover捕获,程序不会宕机。执行完对应的defer后,从宕机点退出当前函数后继续执行。

提示:虽然panic/recover能模拟其他语言的异常机制,但并不建议代表编写普通函数也经常性使用这种特性。

2. 结构体

结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”。字段有以下特性:
● 字段拥有自己的类型和值。
● 字段名必须唯一。
● 字段的类型也可以是结构体,甚至是字段所在结构体的类型。

Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。
Go语言的结构体与“类”都是复合结构体,但Go语言中结构体的内嵌配合接口比面向对象具有更高的扩展性和灵活性。
Go语言不仅认为结构体能拥有方法,且每种自定义类型也可以拥有自己的方法。

2.1 定义与给结构体赋值

基本形式:

type Point struct { X int Y int } var p Point p.X = 10 p.Y = 20

结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存

创建指针类型的结构体:

type Player struct { name string age int } p = new(Player) p.name = "james" p.age = 40

取结构体的地址实例化:

//使用结构体定义一个命令行指令(Command),指令中包含名称、变量关联和注释等 type Command struct { name string Var *int comment string } var version int = 1 cmd := &Command{} cmd.name = "version" cmd.Var = &version cmd.comment = "show version"

使用键值对填充结构体:

type People struct { name string child *People } relation := &People{ name: "爷爷" child: &People{ name: "爸爸" child: &People{ name: "我" }, } } 3. 方法

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收器(Receiver)。
如果将特定类型理解为结构体或“类”时,接收器的概念就类似于其他语言中的this或者self

结构体方法

创建一个背包Bag结构体为其定义把物品放入背包的方法insert

type Bag struct { items[] int } func (b *Bag) insert(itemid int) { b.items = append(b.items, itemid) } func main() { b := new(Bag) b.insert(1001) }

(b*Bag)表示接收器,即Insert作用的对象实例。每个方法只能有一个接收器。

接收器

接收器是方法作用的目标

接收器根据接收器的类型可分:

  • 指针接收器
  • 非指针接收器
  • 两种接收器在使用时会产生不同的效果。根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。
指针接收器

由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的。

// 定义属性结构 type Property struct { value int } // 设置属性值方法 func (p *Property) setVal(val int) { p.value = val } // 获取属性值方法 func (p *Property) getVal() int { return p.value } func main() { p := new(Property) p.value = 123 fmt.Println(p.getVal()) p.setVal(666) fmt.Println(p.getVal()) } 非指针类型接收器

当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份。在非指针接收器的方法中可以获取接收器的成员值,但修改后无效

type Point struct { x, y int } func (p Point) add(other Point) Point { return Point{p.x + other.x, p.y + other.y} } func main() { // 初始化点 p1 := Point{1, 1} p2 := Point{2, 2} res := p1.add(p2) fmt.Println(res) p3 := Point{3, 3} p4 := p1.add(p2).add(p3) fmt.Println(p4) }

指针接收器和非指针接收器的使用:
指针和非指针接收器的使用在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器。大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。

4. 接口

接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。

声明接口

type 接口类型名 interface { 方法1(参数列表) 返回值 ... }

Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer,有关闭功能的接口叫Closer等
方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。

io包中提供的Writer接口:

type Writer interface { Write(p []type) (n int, err error) } 实现接口

实现接口的条件:

  • 接口的方法与实现接口的类型方法格式一致
  • 接口中所有方法均被实现

例:为了抽象数据写入的过程,定义Data Writer接口来描述数据写入需要实现的方法。

// 定义一个数据写入器接口 type DataWriter interface { WriteData(data interface{}) error } // 定义文件结构,用于实现DataWriter type file struct { } // 实现DataWriter接口的方法 func (d *file) WriteData(data interface{}) error { // 模拟写入数据 fmt.Println("Write Data:", data) return nil } func main() { // 实例化file f := new(file) // 声明一个DataWriter接口 var writer DataWriter // 将接口赋值,也就是*file writer = f writer.WriteData("one line data") }

Go语言的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计被称为非侵入式设计。



本文共计3416个文字,预计阅读时间需要14分钟。

Go语言进阶篇:如何深入理解函数、结构体、方法与接口?

目录

1.函数

1.1 函数返回值 1.1.1 同一类型返回值 1.1.2 带变量名的返回值 1.1.3 函数中的参数传递 1.1.4 函数变量 1.2 匿名函数 1.2.1 无函数名函数 1.2.2 定义时调用匿名函数 1.2.3 将匿名函数赋值给变量

目录
  • 1. 函数
    • 1.1 函数返回值
      • 同一种类型返回值
      • 带变量名的返回值
      • 函数中的参数传递
      • 函数变量
    • 1.2 匿名函数——没有函数名字的函数
      • 在定义时调用匿名函数
      • 将匿名函数赋值给变量
      • 匿名函数用作回调函数
      • 可变参数——参数数量不固定的函数形式
    • 1.3 闭包
    • 1.4 defer语句
      • 处理运行时发生的错误
      • 1.5 宕机恢复(recover)——防止程序崩溃
  • 2. 结构体
    • 2.1 定义与给结构体赋值
  • 3. 方法
    • 结构体方法
    • 接收器
      • 指针接收器
      • 非指针类型接收器
  • 4. 接口
    • 声明接口
    • 实现接口

1. 函数

Go语言的函数属于“一等公民”(first-class),也就是说:

  • 函数本身可以作为值进行传递。
  • 支持匿名函数和闭包(closure)。
  • 函数可以满足接口。
1.1 函数返回值 同一种类型返回值

func typedTwoValues() (int, int) { return 1, 2 } a, b := typedTwoValues() fmt.Println(a, b) 带变量名的返回值

func named Ret Values() (a, b int) { a = 1 b = 2 return }

函数使用命名返回值时,可以在return中不填写返回值列表,如果填写也是可行的

函数中的参数传递

Go语言中传入和返回参数在调用和返回时都使用值传递,这里需要注意的是指针、切片和map等引用型对象指向的内容在参数传递中不会发生复制,而是将指针进行复制,类似于创建一次引用。

函数变量

在Go语言中,函数也是一种类型,可以和其他类型一样被保存在变量中

func fire() { fmt.Println("fire") } func main() { var f func() //将变量f声明为func()类型,此时f就被俗称为“回调函数”。此时f的值为nil。 f = fire f() } 1.2 匿名函数——没有函数名字的函数

Go语言支持匿名函数,即在需要使用函数时,再定义函数,匿名函数没有函数名,只有函数体,函数可以被作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式被传递

在定义时调用匿名函数

匿名函数可以在声明后调用,例如:

func(data int) { fmt.Println("hello", data) }(100) 将匿名函数赋值给变量

匿名函数体可以被赋值,例如:

// 将匿名函数体保存到f()中 f := func(data int) { fmt.Println("hello", data) } // 使用f()调用 f(100) 匿名函数用作回调函数

使用时再定义匿名函数,不使用先在被调用函数里面进行声明,这就是回调精髓

// 遍历切片的每个元素,通过给定函数进行元素访问 func visit(list []int, f func(int)) { for _, v := range list { f(v) } } func main() { // 使用匿名函数打印切片内容 visit([]int{1, 2, 3, 4}, func(v int) { fmt.Println(v) }) } 可变参数——参数数量不固定的函数形式

1.所有参数都是可变参数:fmt.Println

func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...) }

fmt.Println在使用时,传入的值类型不受限制,例如:

Go语言进阶篇:如何深入理解函数、结构体、方法与接口?

fmt.Println(5, "hello", &struct{ a int }{1}, true)

当可变参数为interface{}类型时,可以传入任何类型的值

2.部分参数是可变参数:fmt.Printf
fmt.Printf的第一个参数为参数列表,后面的参数是可变参数:

func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...) } ------------------------------------------------------ fmt.Printf("pure string\n") fmt.Printf("value: %v %f\n", true, math.Pi) 1.3 闭包

闭包可以理解成定义在函数内部的一个函数。本质上,闭包是函数内部和函数外部连接起来的桥梁。 简单来说,闭包=函数+引用环境

func main() { var f = add() fmt.Printf("f(10): %v\n", f(10)) fmt.Printf("f(20): %v\n", f(20)) // f(10): 10 // f(20): 30 } func add() func(int) int { var x int return func(y int) int { x += y return x } } 1.4 defer语句

defer语句将其后面跟随的语句进行延迟处理,被defer的语句按先进后出的方式执行(最先defer的语句最后执行,后被defer的语句先执行)。
特性:

  • 关键字defer用于注册延迟调用
  • 直到调用return之前才执行(故可用来作资源清理)
  • 多个defer语句,FILO方式执行
  • defer中的变量,在defer声明时就定义了

用途:

  • 关闭文件句柄
  • 锁资源释放
  • 数据库连接释放
处理运行时发生的错误

Go语言的错误处理思想及设计包含以下特征:
● 一个可能造成错误的函数,需要返回值中返回一个错误接口(error)。如果调用是成功的,错误接口将返回nil,否则返回错误。
● 在函数调用后需要检查错误,如果发生错误,进行必要的错误处理。

错误接口的定义格式
error是Go系统声明的接口类型,代码如下:

type error interface { Error() string // 返回错误的具体描述. }

所有符合Error() string格式的接口都能实现错误接口。

定义一个错误
在Go语言中,使用errors包进行错误的定义,格式如下:

var err = errors.New("this is an error")

错误字符串由于相对固定,一般在包作用域声明,应尽量减少在使用时直接使用errors.New返回。

宕机(panic)——程序终止运行
1 手动触发宕机
Go语言可以在程序中手动触发宕机,让程序崩溃,这样开发者可以及时地发现错误,同时减少可能的损失。
Go语言程序在宕机时,会将堆栈和goroutine信息输出到控制台,所以宕机也可以方便地知晓发生错误的位置。
package main
func main() {
panic("crash")
}

panic()的参数可以是任意类型,

当panic()触发的宕机发生时,panic()后面的代码将不会被运行,但是在panic()函数前面已经运行过的defer语句依然会在宕机发生时发生作用,

1.5 宕机恢复(recover)——防止程序崩溃

无论是代码运行错误由Runtime层抛出的panic崩溃,还是主动触发的panic崩溃,都可以配合defer和recover实现错误捕捉和恢复,让代码在发生崩溃后允许继续运行。

Go没有异常系统,其使用panic触发宕机类似于其他语言的抛出异常,那么recover的宕机恢复机制就对应try/catch机制。

panic和recover的关系:
● 有panic没recover,程序宕机。
● 有panic也有recover捕获,程序不会宕机。执行完对应的defer后,从宕机点退出当前函数后继续执行。

提示:虽然panic/recover能模拟其他语言的异常机制,但并不建议代表编写普通函数也经常性使用这种特性。

2. 结构体

结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”。字段有以下特性:
● 字段拥有自己的类型和值。
● 字段名必须唯一。
● 字段的类型也可以是结构体,甚至是字段所在结构体的类型。

Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。
Go语言的结构体与“类”都是复合结构体,但Go语言中结构体的内嵌配合接口比面向对象具有更高的扩展性和灵活性。
Go语言不仅认为结构体能拥有方法,且每种自定义类型也可以拥有自己的方法。

2.1 定义与给结构体赋值

基本形式:

type Point struct { X int Y int } var p Point p.X = 10 p.Y = 20

结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存

创建指针类型的结构体:

type Player struct { name string age int } p = new(Player) p.name = "james" p.age = 40

取结构体的地址实例化:

//使用结构体定义一个命令行指令(Command),指令中包含名称、变量关联和注释等 type Command struct { name string Var *int comment string } var version int = 1 cmd := &Command{} cmd.name = "version" cmd.Var = &version cmd.comment = "show version"

使用键值对填充结构体:

type People struct { name string child *People } relation := &People{ name: "爷爷" child: &People{ name: "爸爸" child: &People{ name: "我" }, } } 3. 方法

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收器(Receiver)。
如果将特定类型理解为结构体或“类”时,接收器的概念就类似于其他语言中的this或者self

结构体方法

创建一个背包Bag结构体为其定义把物品放入背包的方法insert

type Bag struct { items[] int } func (b *Bag) insert(itemid int) { b.items = append(b.items, itemid) } func main() { b := new(Bag) b.insert(1001) }

(b*Bag)表示接收器,即Insert作用的对象实例。每个方法只能有一个接收器。

接收器

接收器是方法作用的目标

接收器根据接收器的类型可分:

  • 指针接收器
  • 非指针接收器
  • 两种接收器在使用时会产生不同的效果。根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。
指针接收器

由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的。

// 定义属性结构 type Property struct { value int } // 设置属性值方法 func (p *Property) setVal(val int) { p.value = val } // 获取属性值方法 func (p *Property) getVal() int { return p.value } func main() { p := new(Property) p.value = 123 fmt.Println(p.getVal()) p.setVal(666) fmt.Println(p.getVal()) } 非指针类型接收器

当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份。在非指针接收器的方法中可以获取接收器的成员值,但修改后无效

type Point struct { x, y int } func (p Point) add(other Point) Point { return Point{p.x + other.x, p.y + other.y} } func main() { // 初始化点 p1 := Point{1, 1} p2 := Point{2, 2} res := p1.add(p2) fmt.Println(res) p3 := Point{3, 3} p4 := p1.add(p2).add(p3) fmt.Println(p4) }

指针接收器和非指针接收器的使用:
指针和非指针接收器的使用在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器。大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。

4. 接口

接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。

声明接口

type 接口类型名 interface { 方法1(参数列表) 返回值 ... }

Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer,有关闭功能的接口叫Closer等
方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。

io包中提供的Writer接口:

type Writer interface { Write(p []type) (n int, err error) } 实现接口

实现接口的条件:

  • 接口的方法与实现接口的类型方法格式一致
  • 接口中所有方法均被实现

例:为了抽象数据写入的过程,定义Data Writer接口来描述数据写入需要实现的方法。

// 定义一个数据写入器接口 type DataWriter interface { WriteData(data interface{}) error } // 定义文件结构,用于实现DataWriter type file struct { } // 实现DataWriter接口的方法 func (d *file) WriteData(data interface{}) error { // 模拟写入数据 fmt.Println("Write Data:", data) return nil } func main() { // 实例化file f := new(file) // 声明一个DataWriter接口 var writer DataWriter // 将接口赋值,也就是*file writer = f writer.WriteData("one line data") }

Go语言的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计被称为非侵入式设计。