Go学习第五章——函数与包

1 函数

函数是一段可以重复执行的代码块,通过函数可以将代码模块化,提高代码的可读性和可维护性。

要定义函数,需要指定函数的名称、参数和返回值(如果有的话)。

1.1 基本语法

基本语法

func   函数名(形参列表)(返回值类型列表){
	执行语句..
	return + 返回值列表
}

下面是一个简单的示例,展示了如何定义和调用一个简单的函数:

package main

import "fmt"

// 定义一个名为greeting的函数,它接收一个字符串参数name并没有返回值
func greeting(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

func main() {
    // 调用greeting函数,传入一个名字作为参数
    greeting("Alice")
}

运行上面的代码,输出结果为:

Hello, Alice!
1.2 函数多返回值

在 Go 语言中,函数可以返回多个值。这在某些情况下很有用,例如一个函数需要返回多个计算结果,或者需要返回一个值和一个错误状态。

下面是一个示例,展示了如何定义和使用返回多个值的函数:

package main

import "fmt"

// 定义一个名为divide的函数,它接收两个整数参数,并返回一个商和余数
func divide(a, b int) (int, int) {
    quotient := a / b
    remainder := a % b
    return quotient, remainder
}

func main() {
    // 调用divide函数,并接收两个返回值
    q, r := divide(10, 3)
    fmt.Printf("商:%d,余数:%d\n", q, r)
}

运行上面的代码,输出结果为:

商:3,余数:1

通过返回多个值,函数的调用方可以方便地获得函数计算的多个结果。

1.3 函数的可见性和包级函数

在 Go 语言中,函数和变量的可见性是由它们的命名规则决定的。一个函数或变量是否对其他代码可见,取决于它们的名称是否以大写字母开头

如果一个函数或变量的名称以大写字母开头,则它对其他代码可见;如果名称以小写字母开头,则它只对同一个包内的代码可见。

下面是一个示例,展示了可见性的规则:

package main

import "fmt"

// 可以被其他代码访问
func PublicFunc() {
    fmt.Println("公有函数")
}

// 只能在当前包内访问
func privateFunc() {
    fmt.Println("私有函数")
}

func main() {
    PublicFunc()
    privateFunc() // 错误:无法访问私有函数
}

在这个示例中,我们定义了一个名为 PublicFunc 的公有函数,以及一个名为 privateFunc 的私有函数。在 main 函数中,我们可以正常调用 PublicFunc,但无法调用 privateFunc

按照这个规则,我们可以将一些公共的、被其他代码调用的函数定义为包级函数,并将一些内部函数定义为私有函数。这有助于将代码逻辑与实现细节隔离,并提高代码的封装性。

1.4 函数调用机制底层原理
  1. 执行n1 := 10,会生成一个存储这个值的区域,这里只是抽象为有这么一个main栈区,实际上不是这样命名,是使用寄存器和栈帧来实现,具体不讲解
  2. 因为是栈的方式,后进先出,所以这里调用函数后占用的数据,也会被优先回收掉。

在这里插入图片描述

函数调用是计算机程序中的一个重要概念,它用于在程序执行过程中跳转到函数代码的起始位置,并在函数执行完毕后返回到原来的位置。函数调用的底层实现涉及到栈的分配、参数传递和返回值处理等过程。

  1. 栈空间的分配:每个线程都会有自己的栈空间,用于存储函数的局部变量、函数参数和返回值。函数调用时,会给调用栈分配一块空间来存储函数执行过程中所需的数据。栈的分配是一个后进先出(LIFO)的过程,即新的函数调用会在栈的顶部分配空间

  2. 参数传递:函数调用需要将参数传递给被调用的函数。参数的传递方式一般分为两种:值传递和引用传递

  • 在值传递中,参数的值会被复制到被调用函数的栈帧中,由于是复制操作,被调函数的修改不会影响到调用函数。

  • 在引用传递中,函数参数是一个指针,传递的是变量在内存中的地址,被调用函数可以通过指针来访问和修改原始数据。

  1. 函数调用过程:当一个函数需要调用另一个函数时,会先将当前函数的执行状态压入栈中,包括返回地址、参数、局部变量等信息。然后跳转到被调用函数的起始位置,执行被调用函数的代码。在被调用函数执行结束后,会将返回值返回给调用函数,在栈中恢复调用函数的执行状态,包括返回地址和栈帧等信息,然后继续执行。

  2. 返回值处理:函数执行完毕后,需要将返回值返回给调用函数。返回值的处理方式和参数传递类似,可以使用值传递或者引用传递。在实际的底层实现中,一般通过寄存器或者栈帧来传递和存储返回值。

总结起来,函数调用的底层实现主要涉及栈空间的分配与释放、参数传递和返回值处理等过程。这些过程是通过寄存器和栈帧来实现的,不同的编程语言和编译器可能会有一些细节上的差异,但基本思想是相通的。

1.5 值类型和引用类型

前面使用的值传输,所以会发现函数并没有修改值,只是修改函数本身的栈空间里的变量值,当然还有相对应,可以直接通过引用类型传递,修改对应的值。

使用传输地址,也就是引用传递的方式,让函数直接修改地址值。

import "fmt"

func add1(num int) {
	num = num + 1
	fmt.Println("在add1函数里,num=", num)
}

func add2(num *int) {
	*num = *num + 1
	fmt.Println("在add2函数里,num=", *num)
}

func main() {
	var num int = 2
	add1(num)
	fmt.Println("在main函数里,第一次,num=", num)
	add2(&num)
	fmt.Println("在main函数里,第二次,num=", num)
}

输出结果:

在add1函数里,num= 3
在main函数里,第一次,num= 2
在add2函数里,num= 3
在main函数里,第二次,num= 3
1.6 注意事项和细节
  1. golang不支持重载(通过其他方式实现)
  2. 在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。
  3. 函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用。
  4. 为了简化数据类型定义,Go支持自定义数据类型
  • 基本语法:type 自定义数据类型名 数据类型 // 理解:相当于一个别名
  • 案例:type myInt int // 这是myInt就等价int来使用了
  • 案例:type mySum func(int, int) int // 这是mySum就等价一个函数类型func (int, int) int
import "fmt"

type myFunType func(int, int) int

func myFun(funvar myFunType, num1 int, num2 int) int {
	return funvar(num1, num2)
}

func main() {
	type myInt int

	var num1 myInt = 40
	// num2 := int(num1) // 报错,因为myInt本质是,不是一个数据类型
	fmt.Println("num1=", num1)

	res := myFun(func(i1 int, i2 int) int { // 这里用到了匿名函数,可以去看看后面所说的定义
		return i1 + i2
	}, 500, 600)
	fmt.Println("res=", res)
}

输出:

num1= 40
res= 1100
  1. 支持对函数返回值命名

cal1和cal2的函数是一样的,只是cal2里对返回值命名,这样就可以不用考虑顺序,因为如果是cal1的方式,return的值需要考虑跟返回值类型的顺序一样。

import (
	"fmt"
	"strconv"
)

func cal1(n1 int, n2 int) (int, string) {
	sum := n1 + n2
	sub := strconv.FormatInt(int64(n1-n2), 10)
	return sum, sub
}

func cal2(n1 int, n2 int) (sum int, sub string) {
	sub = strconv.FormatInt(int64(n1-n2), 10)
	sum = n1 + n2
	return
}

func main() {
	sum1, sub1 := cal1(2, 2)
	sum2, sub2 := cal2(2, 2)

	fmt.Printf("cal1的返回值:%v, %v \n", sum1, sub1)
	fmt.Printf("cal2的返回值:%v, %v \n", sum2, sub2)
}

输出结果:

cal1的返回值:4, 0
cal2的返回值:4, 0
  1. 使用_标识符,忽略返回值
import "fmt"

func cal(n1 int, n2 int) (sum int, sub int) {
	sum = n1 + n2
	sub = n1 - n2
	return
}

func main() {
	res1, _ := cal(10, 20)
	fmt.Printf("res1 = %d", res1)
}

输出:res1 = 30

1.7 逃逸机制(补,可不看)

逃逸分析是编译器的一种静态分析技术,用于分析程序中的变量是否会逃逸到堆上分配内存。逃逸指的是当一个变量在函数内部分配内存,并且在函数外部被引用时,该变量就会逃逸到堆上。

逃逸分析的作用是优化内存分配和回收,将一部分变量从堆上分配转移到栈上分配,减少堆的压力和垃圾回收的负担。逃逸分析可以减少内存分配的次数,避免频繁的系统调用和锁竞争,提高程序的性能和并发能力。

逃逸分析的实现原理可以分为以下几个步骤:

  1. 内联优化:逃逸分析通常是在函数级别上进行的,首先编译器会尝试内联函数。内联优化是将函数的代码插入到调用它的函数中,减少函数调用的开销。内联优化会扩展函数的作用域,使函数内部的变量和参数可以直接访问。这样,一些局部变量就可以在栈上分配,而不是在堆上分配。

  2. 变量分析:逃逸分析会对函数的变量进行分析,判断变量是否逃逸。如果一个变量逃逸,编译器会将其分配在堆上;如果一个变量不逃逸,则可以将其分配在栈上。变量的逃逸分析包括以下情况的判断:

    • 变量是否在函数返回后继续存在;
    • 变量是否被存储到全局变量中,以供其他函数使用;
    • 变量是否被闭包函数引用。
  3. 逃逸分析结果的使用:逃逸分析的结果会被编译器用于指导内存分配器进行内存分配。根据逃逸分析的结果,编译器可以决定将变量分配在栈上还是堆上。对于不逃逸的变量,编译器可以直接在栈上分配内存,避免了堆分配和垃圾回收的开销。

总结起来,逃逸分析是一种优化技术,它使用静态分析的方法判断变量是否会逃逸到堆上分配内存。逃逸分析的作用是减少堆的压力和垃圾回收的负担,提高程序的性能和并发能力。逃逸分析的实现原理包括内联优化、变量分析和逃逸分析结果的使用。

2 包

go的每一个文件都是属于一个包,也就是说go是以包的形式来管理文件和项目目录结构。

2.1 快速入门

包的三大作用:

  1. 区分相同名字的很熟、变量等标识符
  2. 当程序文件很多时,可以很好的管理项目
  3. 控制函数、变量等访问范围,即作用域

首先创建不同文件夹下面的包,再编写好对应的函数,然后import导入,最后调用!

在这里插入图片描述

2.2 包的使用细节
  1. 在给一个文件夹打包时,该包对应一个文件夹,比如这里的是utils文件夹对应的包名就是utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母。

  2. 当包名过长时,可以给包名取别名,取完之后,之前的就不能用了

在import时,在前面写的代码,就是别名。例如:这里把utils改成util。

package main

import (
	util "GoStudy_Day1/Day03/model/utils"
	"fmt"
)

func main() {
	var num int = 2
	nums := util.Cal(num)
	fmt.Printf("%v 的平方等于:%v \n", num, nums)
}
  1. 编译一个可执行程序文件,就需要将这个包声明为main,然后实际开发的时候,都是编译成exe文件再运行。

编译可以指定名字和目录,比如:放在bin目录下:go build -o bin/my.exe go_code/project/main

3 函数详细讲解

3.1 递归调用

在函数里,又调用了本身,也就是自己

下面是一个案例和底层栈空间的调用情况

在这里插入图片描述

注意细节:

  1. 执行一个函数时,就会串接一个新的受保护的独立空间(新函数栈,如图所示)
  2. 函数的局部变量是独立的,不会相互影响
  3. 递归必须向退出递归的条件逼近,否则就是无限递归,很容易栈溢出崩溃,实际开发不怎么用
  4. 当一个函数执行完毕或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时函数执行完毕就销毁。
3.2 可变函数参数

可变参数函数是指可以接收不定数量参数的函数,这些参数被看作是一个切片。

Go 语言中的可变参数函数使用 ... 表示。

下面是一个示例,展示了如何定义和调用可变参数函数:

package main

import "fmt"

// 定义一个名为sum的函数,它接收任意数量的整数参数,并返回它们的总和
func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

func main() {
    // 调用sum函数,传入多个整数参数
    fmt.Println("总和:", sum(1, 2, 3, 4, 5))
    fmt.Println("总和:", sum(10, 20, 30))
}

运行上面的代码,输出结果为:

总和: 15
总和: 60

在这个示例中,我们定义了一个名为 sum 的函数,它可以接收任意数量的整数参数,并返回它们的总和。在 main 函数中,我们调用了 sum 函数两次,并传递了不同数量的参数。

通过使用可变参数,我们可以简化函数的调用,使之更加灵活。

3.3 init 函数

基本介绍:

每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说init会在main函数前被调用。

案例说明:

import "fmt"

func init() {
	fmt.Println("main init...")
}

func main() {
	fmt.Println("main...")
}

输出结果:

main init...
main... 

细节讨论:

  1. 如果一个文件同时包含全局变量定义,init函数和main函数,则执行的流程是变量定义->init函数->main函数
  2. init函数最主要的作用,就是完成一些初始化的工作,比如下面的案例:

utils包:

package utils

var Name string
var Age int

func init() {
    Name = "Tom"
    Age = 100
}

main包

import (
	"GoStudy_Day1/Day03/model/utils"
	"fmt"
)

func main() {
	fmt.Printf("Name = %v, Age = %v", utils.Name, utils.Age)
}

输出结果:Name = Tom, Age = 100

从过程可以看出,有点像Java定义成员变量,并且给一个默认值的感觉

3.4 匿名函数

匿名函数是一种特殊的函数,它没有函数名,可以直接在其他函数中定义和使用。匿名函数在需要临时定义一段代码,并且这段代码不需要复用时很有用。

下面是示例,展示了如何定义和调用匿名函数:

实例一:

package main

import "fmt"

func main() {
    // 在main函数内定义一个匿名函数,并立即调用它
    func() {
        fmt.Println("这是一个匿名函数!")
    }()

    // 将匿名函数赋值给变量,然后进行调用
    greeting := func(name string) {
        fmt.Printf("Hello, %s!\n", name)
    }
    greeting("Alice")
}

运行上面的代码,输出结果为:

这是一个匿名函数!
Hello, Alice!

实例二:将匿名函数赋给a变量

func main() {
    a := func(n1 int, n2 int) int {
       return n1 - n2
    }

    res2 := a(10, 10)
    fmt.Println("res2=", res2)
}

输出结果:res2= 0

实例三:匿名函数赋值给全局变量,那么就成为一个全局匿名函数

这里不写了,跟上面雷同~~~

匿名函数可以用于需要临时定义一段代码的场景,例如在并发编程中,可以将匿名函数传递给协程进行并发执行。

3.5 闭包

闭包是指一个函数捕获并保存了其自身外部作用域的变量的引用。

简单来说,闭包就是一个函数以及它所引用的变量的组合体。

下面是一个示例,展示了如何使用闭包:

package main

import "fmt"

// 定义一个名为counter的函数,返回一个匿名函数
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    // 创建一个计数器实例
    c := counter()

    // 使用闭包进行计数
    fmt.Println(c()) // 输出:1
    fmt.Println(c()) // 输出:2
    fmt.Println(c()) // 输出:3
}

运行上面的代码,输出结果为:

1
2
3

在这个示例中,我们定义了一个 counter 函数,它返回一个匿名函数。在匿名函数内部,我们定义了一个变量 count,然后在每次调用匿名函数时更新这个变量,并返回它的值。

最佳实践

假设传入一个文件名,设置一个变量作为文件名的后缀,如果这个文件名没有指定的后缀,则自动给这个文件名添加后缀,如果有的话,直接输出。

这里需要使用HasSuffix函数,表示查找该该string有没有指定的后缀

func makeSuffix(suffix string) func(string) string {
	return func(name string) string {
		// 如果 name 没有指定后缀,则加上,否则就返回原来的名字
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		}

		return name
	}
}

func main() {
	f := makeSuffix(".jpg")
	fmt.Println("文件名处理后=", f("winter"))
	fmt.Println("文件名处理后=", f("winter.jpg"))
}

输出结果:

文件名处理后= winter.jpg
文件名处理后= winter.jpg

虽然也可以用普通函数实现,但是过程太过复杂,所以不用。

通过闭包,我们可以创建一个状态被隐藏的函数,这个函数可以持续地访问和修改它所引用的变量。

ps:闭包还能通过这方式实现协程之间的数据传递,不过需要避免变量共享问题。(以后再讲~~)

3.6 函数作为参数和返回值

在 Go 语言中,函数可以作为参数传递给其他函数,也可以作为函数的返回值。这种能力使得代码更加灵活,可以根据需要将函数与其他函数进行组合。

下面是一个示例,展示了如何将函数作为参数和返回值:

package main

import "fmt"

// 定义一个名为apply的函数,它接收一个函数作为参数,并将参数函数应用到数字5上
func apply(f func(int, int) int) {
    result := f(5, 10)
    fmt.Println("应用结果:", result)
}

// 定义一个名为add的函数,它接收两个整数并返回它们的和
func add(a, b int) int {
    return a + b
}

func main() {
    // 将add函数作为参数传递给apply函数
    apply(add)

    // 将匿名函数作为参数传递给apply函数
    apply(func(a, b int) int {
        return a * b
    })
}

运行上面的代码,输出结果为:

应用结果: 15
应用结果: 50

在这个示例中,我们定义了一个 apply 函数,它接收一个函数作为参数,并将这个函数应用到数字5和10上。在 main 函数中,我们分别将 add 函数和一个匿名函数作为参数传递给 apply 函数,用于实现加法和乘法操作。

通过将函数作为参数和返回值,我们可以更灵活地组合和使用函数,实现更多复杂的功能。

3.7 defer函数

为什么需要defer

在函数中,程序员进程需要创建资源(比如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer(延时机制)。

在 Go 语言中,我们可以使用 defer 关键字延迟执行一些代码,无论外部函数执行的怎样,这些代码都会在函数返回之前被执行。

下面是一个示例,展示了 defer 的使用场景:

import "fmt"

func sum(n1 int, n2 int) int {
	defer fmt.Println("ok1 n1=", n1)
	defer fmt.Println("ok2 n2=", n2)
	res := n1 + n2
	fmt.Println("ok3 res=", res)
	return res
}

func main() {
	res := sum(10, 20)
	fmt.Println("res=", res)
}

运行上面的代码,输出结果为:

ok3 res= 30
ok2 n2= 20
ok1 n1= 10
res= 30

当go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中,当函数执行完毕之后,再从defer栈中,一次从栈顶取出语句执行,所以从上面的案例就可以看出,先执行的是ok2语句。

3.8 作为结构体的方法

在 Go 语言中,方法是一种与特定类型关联的函数。它们可以通过定义在类型上的方法来实现某些特定操作。

下面是一个示例,展示了如何定义和使用方法:

package main

import (
    "fmt"
    "math"
)

// 定义一个名为Circle的结构体类型
type Circle struct {
    radius float64
}

// 在Circle类型上定义一个名为area的方法,它返回这个圆的面积
func (c Circle) area() float64 {
    return math.Pi * c.radius * c.radius
}

func main() {
    // 创建一个Circle实例
    c := Circle{radius: 5}

    // 调用Circle类型的方法
    fmt.Println("圆的面积:", c.area())
}

运行上面的代码,输出结果为:

圆的面积: 78.53981633974483

在这个示例中,我们定义了一个名为 Circle 的结构体类型,它包含一个半径属性。然后,在 Circle 类型上定义了一个名为 area 的方法,它用于计算圆的面积。

main 函数中,我们创建了一个 Circle 实例,并调用了 Circle 类型的 area 方法来计算面积。

通过方法,我们可以将某些操作与特定类型绑定,使得代码更加清晰和面向对象。

4 变量的作用域

  1. 函数内部声明/定义的遍历叫局部变量,作用域仅限于函数内部。

    func test() {
    	// age 和 name 的作用域就只在test函数内部
    	age := 10
    	Name := "Tom~"
    }
    
    func main() {
    }
    
  2. 函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效。

    //函数外部声明/定义的变量叫全局变量,
    //作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效
    var age int = 50
    var Name string = "jack~"
    
    //函数
    func test() {
    	//age 和 Name的作用域就只在test函数内部
    	age := 10
    	Name := "tom~"
    	fmt.Println("age=", age) // 10
    	fmt.Println("Name=", Name) // tom~
    }
    
    func main() {
    
    	fmt.Println("age=", age) //  50
    	fmt.Println("Name=", Name) // jack~
    	test()
    }
    
  3. 如果变量是在一个代码块,比如 for / if中,那么这个变量的作用域就在该代码块。

package main
import (
	"fmt"
)
func main() {

	//如果变量是在一个代码块,比如 for / if中,那么这个变量的的作用域就在该代码块

	for i := 0; i <= 10; i++ {
		fmt.Println("i=", i)
	}

	var i int //局部变量
	for i = 0; i <= 10; i++ {
		fmt.Println("i=", i)
	}

	fmt.Println("i=", i)
}

Over~~~~结束啦!!!!冲冲冲!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值