go语言学习之函数与指针

我爱海鲸 2023-07-02 16:10:58 go语言学习

简介go

函数

(⼀)、什么是函数
1、函数是组织好的、可重复使⽤的执⾏特定任务的代码块。它可以提⾼应⽤程
序的模块性和代码的重复利⽤率。
2、Go语⾔⽀持普通函数、匿名函数和闭包,从设计上对函数进⾏了优化和改
进,让函数使⽤起来更加⽅便。
3、Go语⾔的函数属于⼀等公⺠(first-class):
函数本身可以作为值进⾏传递;
⽀持匿名函数和闭包(closure);
函数可以满⾜接⼝。

(⼆)、声明函数
1、函数声明的作⽤
普通函数需要先声明才能调⽤,⼀个函数的声明包括参数和函数名等。编
函数与指针
译器通过声明才能了解函数应该怎样在调⽤代码和函数体之间传递参数和返
回参数。
2、语法格式:
func 函数名(参数列表)(返回参数列表){
//函数体

func funcName (parametername type1, parametername type2...) 
(output1 type1, output2 type2...) {
 //逻辑代码
 //返回多个值
 return value1, value2...
 }

3、函数定义解析:
func:函数关键字。
函数由 func 开始声明
funcName:函数名。
函数名和参数列表⼀起构成了函数签名。
函数名由字⺟、数字和下划线组成。函数名的第⼀个字⺟不能为
数字。在同⼀个包内,函数名称不能重名。
parametername type:参数列表。
参数就像⼀个占位符,定义函数时的参数叫做形式参数,形参变
量是函数的局部变量;当函数被调⽤时,你可以将值传递给参数,
这个值被称为实际参数。
参数列表指定的是参数类型、顺序、及参数个数。
参数是可选的,也就是说函数也可以不包含参数。
参数类型的简写
在参数列表中,如果有多个参数变量,则以逗号分隔;如果
相邻变量是同类型,则可以将类型省略。
例如:func add (a , b int) {}
Go语⾔的函数⽀持可变参数。接受变参的函数是有着不定数量的
参数的。
func myfunc(arg ...int) {}
arg ...int告诉Go这个函数接受不定数量的参数。注意,这
些参数的类型全部是int。在函数体中,变量arg是⼀个int的
slice。
output1 type1, output2 type2:返回值列表。
返回值返回函数的结果,结束函数的执⾏。
Go语⾔的函数可以返回多个值。
返回值可以是:返回数据的数据类型,或者是:变量名+变量类
型的组合。
函数声明时有返回值,必须在函数体中使⽤return语句提供返回
值列表。
如果只有⼀个返回值且不声明返回值变量,那么可以省略包括返
回值的括号。
return后的数据,要保持和声明的返回值类型、数量、顺序⼀
致。
如果函数没有声明返回值,函数中也可以使⽤return关键字,⽤
于强制结束函数。
函数体:函数定义的代码集合,是能够被重复调⽤的代码⽚段。

(三)、变量作⽤域
1、概述
作⽤域是变量、常量、类型、函数的作⽤范围。
Go 语⾔中变量可以在三个地⽅声明:
函数内定义的变量称为局部变量
函数外定义的变量称为全局变量
函数中定义的参数称为形式参数
2、局部变量
在函数体内声明的变量称之为局部变量,它们的作⽤域只在函数体内,参
数和返回值变量也是局部变量。
3、全局变量
在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚⾄外部
包(被导出后)使⽤。
全局变量可以在任何函数中使⽤。Go 语⾔程序中全局变量与局部变量名称可以
相同,但是函数内的局部变量会被优先考虑。
4、形式参数
形式参数会作为函数的局部变量来使⽤。
5、案例分析

package main
import "fmt"
/* 声明全局变量 */
var a1 int = 7
var b1 int = 9
func main() {
 /* main 函数中声明局部变量 */
 a1, b1, c1 := 10, 20, 0
 fmt.Printf("main()函数中 a1 = %d\n", a1) //10
 fmt.Printf("main()函数中 b1 = %d\n", b1) //20
 fmt.Printf("main()函数中 c1 = %d\n", c1) //0
 c1 = sum(a1, b1)
 fmt.Printf("main()函数中 c1 = %d\n", c1) //33
}
/* 函数定义-两数相加 */
func sum(a1, b1 int) (c1 int) {
 a1++
 b1 += 2
 c1 = a1 + b1
 fmt.Printf("sum() 函数中 a1 = %d\n", a1) //11
 fmt.Printf("sum() 函数中 b1 = %d\n", b1) //22
 fmt.Printf("sum() 函数中 c1 = %d\n", c1) //33
 return c1
}

输出结果:
main()函数中 a1 = 10
main()函数中 b1 = 20
main()函数中 c1 = 0
sum() 函数中 a1 = 11
sum() 函数中 b1 = 22
sum() 函数中 c1 = 33
main()函数中 c1 = 33

(四)、函数变量(函数作为值)
在Go语⾔中,函数也是⼀种类型,可以和其它类型⼀样被保存在变量
中。
可以通过type来定义⼀个⾃定义类型。函数的参数完全相同(包括:参数
类型、个数、顺序),函数返回值相同。
1、案例代码⼀:

package main
import (
 "fmt"
 "strings"
)
func main() {
 result := StringToLower("AbcdefGHijklMNOPqrstUVWxyz", processCase)
 fmt.Println(result)
 result = StringToLower2("AbcdefGHijklMNOPqrstUVWxyz", processCase)
 fmt.Println(result)
}
//处理字符串,奇数偶数依次显示为⼤⼩写
func processCase(str string) string {
 result := ""
 for i, value := range str {
 if i%2 == 0 {
 result += strings.ToUpper(string(value))
 } else {
 result += strings.ToLower(string(value))
 }
 }
 return result
}
func StringToLower(str string, f func(string) string) string {
 fmt.Printf("%T \n", f)
 return f(str)
}
type caseFunc func(string) string // 声明了⼀个函数类型。通过type关键字,
caseFunc会形成⼀种新的类型。
func StringToLower2(str string, f caseFunc) string {
 fmt.Printf("%T \n", f)
 return f(str)
}

2、案例代码⼆:

package main
import "fmt"
type processFunc func(int) bool // 声明了⼀个函数类型
func main() {
 slice := []int{1, 2, 3, 4, 5, 7}
 fmt.Println("slice = ", slice)
 odd := filter(slice, isOdd) // 函数当做值来传递
 fmt.Println("奇数元素: ", odd)
 even := filter(slice, isEven) // 函数当做值来传递
 fmt.Println("偶数元素: ", even)
}
//判断元素是否是偶数
func isEven(integer int) bool {
 if integer%2 == 0 {
 return true
 }
 return false
}
//判断元素是否是奇数
func isOdd(integer int) bool {
 if integer%2 == 0 {
 return false
 }
 return true
}
//根据函数来处理切⽚,根据元素奇数偶数分组,返回新的切⽚
func filter(slice []int, f processFunc) []int {
 var result []int
 for _, value := range slice {
 if f(value) {
 result = append(result, value)
 }
 }
 return result
}

3、函数变量的使⽤步骤及意义:
1. 定义⼀个函数类型
2. 实现定义的函数类型
3. 作为参数调⽤
函数变量的⽤法类似接⼝的⽤法。
函数当做值和类型在写⼀些通⽤接⼝的时候⾮常有⽤,通过上⾯例⼦可以
看到processFunc这个类型是⼀个函数类型,然后两个filter函数的参数和返
回值与processFunc类型是⼀样的。⽤户可以实现很多种的逻辑,这样使得
程序变得⾮常的灵活。
(五)、匿名函数
1、概念
Go语⾔⽀持匿名函数,即在需要使⽤函数时,再定义函数,匿名函数没
有函数名,只有函数体,函数可以被作为⼀种类型被赋值给变量,匿名函数
也往往以变量⽅式被传递。
匿名函数经常被⽤于实现回调函数、闭包等。
2、定义格式
func(参数列表) (返回参数列表) {
//函数体
}
3、定义匿名函数
(1)、在定义时调⽤匿名函数

package main
import "fmt"
func main() {
 func(data int) {
 fmt.Println("hello" , data)
 }(100)
}

(2)、将匿名函数赋值给变量

package main
import "fmt"
func main() {
 f:= func(data string) {
 fmt.Println(data)
 }
 f("欢迎学习Go语⾔!")
}

4、匿名函数的⽤法——作回调函数

package main
import (
 "fmt"
 "math"
)
func main() {
 //调⽤函数,对每个元素进⾏求平⽅根操作
 arr := []float64{1, 9, 16, 25, 30}
 visit(arr, func(v float64) {
 v = math.Sqrt(v)
 fmt.Printf("%.2f \n", v)
 })
 //调⽤函数,对每个元素进⾏求平⽅操作
 visit(arr, func(v float64) {
 v = math.Pow(v , 2)
 fmt.Printf("%.0f \n", v)
 })
}
//定义⼀个函数,遍历切⽚元素,对每个元素进⾏处理
func visit(list []float64, f func(float64)) {
 for _, value := range list {
 f(value)
 }
}

(六)、闭包
1、概念:
闭包并不是什么新奇的概念,它早在⾼级语⾔开始发展的年代就产⽣了。
闭包(Closure)是词法闭包(Lexical Closure)的简称。对闭包的具体定
义有很多种说法,⼤体可以分为两类:
⼀种说法认为闭包是符合⼀定条件的函数,⽐如这样定义闭包:闭包
是在其词法上下⽂中引⽤了⾃由变量的函数。
另⼀种说法认为闭包是由函数和与其相关的引⽤环境组合⽽成的实
体。⽐如这样的定义:在实现深约束时,需要创建⼀个能显式表示引⽤
环境的东⻄,并将它与相关的⼦程序捆绑在⼀起,这样捆绑起来的整体
被称为闭包。函数 + 引⽤环境 = 闭包
上⾯的定义,⼀个认为闭包是函数,另⼀个认为闭包是函数和引⽤环境组
成的整体。显然第⼆种说法更确切。闭包只是在形式和表现上像函数,但实
际上不是函数。
函数是⼀些可执⾏的代码,这些代码在函数被定义后就确定了,不会
在执⾏时发⽣变化,所以⼀个函数只有⼀个实例。
闭包在运⾏时可以有多个实例,不同的引⽤环境和相同的函数组合可
以产⽣不同的实例。
闭包在某些编程语⾔中被称为Lambda表达式。
函数本身不存储任何信息,只有与引⽤环境结合后形成的闭包才具有“记
忆性”。函数是编译器静态的概念,⽽闭包是运⾏期动态的概念。
对象是附有⾏为的数据,⽽闭包是附有数据的⾏为。
2、闭包的价值
(1)、加强模块化
闭包有益于模块化编程,它能以简单的⽅式开发较⼩的模块,从⽽提⾼开
发速度和程序的可复⽤性。和没有使⽤闭包的程序相⽐,使⽤闭包可将模块
划分得更⼩。
⽐如我们要计算⼀个数组中所有数字的和,这只需要循环遍历数组,把遍
历到的数字加起来就⾏了。如果现在要计算所有元素的积呢?要打印所有的
元素呢?解决这些问题都要对数组进⾏遍历,如果是在不⽀持闭包的语⾔
中,我们不得不⼀次⼜⼀次重复地写循环语句。⽽这在⽀持闭包的语⾔中是
不必要的。这种处理⽅法多少有点像回调函数,不过要⽐回调函数写法更简
单,功能更强⼤。
(2)、抽象
闭包是数据和⾏为的组合,这使得闭包具有较好抽象能⼒。
(3)、简化代码
3、⼀个编程语⾔需要哪些特性来⽀持闭包呢?
函数是⼀阶值(First-class value,⼀等公⺠),即函数可以作为另⼀个
函数的返回值或参数,还可以作为⼀个变量的值。
函数可以嵌套定义,即在⼀个函数内部可以定义另⼀个函数。
允许定义匿名函数。
可以捕获引⽤环境,并把引⽤环境和函数代码组成⼀个可调⽤的实体;
4、案例代码⼀
(1)、没有使⽤闭包进⾏计数的代码

package main
import "fmt"
func main() {
 for i := 0; i < 5; i++ {
 fmt.Printf("i=%d \t", i)
 fmt.Println(add2(i))
 }
}
func add2(x int) int {
 sum := 0
 sum += x
 return sum
}

运⾏结果:
i=0 0
i=1 1
i=2 2
i=3 3
i=4 4
for循环每执⾏⼀次,sum都会清零,没有实现sum累加计数。

(2)、使⽤闭包函数实现计数器:

package main
import "fmt"
func main() {
 pos := adder()
 for i := 0; i < 10; i++ {
 fmt.Printf("i=%d \t", i)
 fmt.Println(pos(i))
 }
 fmt.Println("---------------------")
 for i := 0; i < 10; i++ {
 fmt.Printf("i=%d \t", i)
 fmt.Println(pos(i))
 }
}
func adder() func(int) int {
 sum := 0
 return func(x int) int {
 fmt.Printf("sum1=%d \t", sum)
 sum += x
 fmt.Printf("sum2=%d \t", sum)
 return sum
 }
}

运⾏结果为:
i=0 sum1=0 sum2=0 0
i=1 sum1=0 sum2=1 1
i=2 sum1=1 sum2=3 3
i=3 sum1=3 sum2=6 6
i=4 sum1=6 sum2=10 10
---------------------
i=0 sum1=10 sum2=10 10
i=1 sum1=10 sum2=11 11
i=2 sum1=11 sum2=13 13
i=3 sum1=13 sum2=16 16
i=4 sum1=16 sum2=20 20

5、案例代码⼆

package main
import "fmt"
func main() {
 myfunc := Counter()
 //fmt.Printf("%T\n", myfunc)
 fmt.Println("myfunc", myfunc)
 /* 调⽤ myfunc 函数,i 变量⾃增 1 并返回 */
 fmt.Println(myfunc())
 fmt.Println(myfunc())
 fmt.Println(myfunc())
 /* 创建新的函数 nextNumber1,并查看结果 */
 myfunc1 := Counter()
 fmt.Println("myfunc1", myfunc1)
 fmt.Println(myfunc1())
 fmt.Println(myfunc1())
}
//计数器.闭包函数
func Counter() func() int {
 i := 0
 res := func() int {
 i += 1
 return i
 }
 //fmt.Printf("%T , %v \n" , res , res) //func() int , 0x1095af0
 fmt.Println("Counter中的内部函数:", res) //0x1095af0
 return res
}

(七)、可变参数
1、如果⼀个函数的参数,类型⼀致,但个数不定,可以使⽤函数的可变参数。
2、语法格式:
func 函数名(参数名 ...类型) [(返回值列表)] {
//函数体
}
该语法格式定义了⼀个接受任何数⽬、任何类型参数的函数。这⾥特殊的
语法是三个点“...”,在⼀个变量后⾯加上三个点后,表示从该处开始接受不
定参数。
当要传递若⼲个值到不定参数函数中得时候,可以⼿动书写每个参数,也
可以将⼀个slice传递给该函数,通过"..."可以将slice中的参数对应的传递给
函数。
3、案例代码:计算学员考试总成绩及平均成绩

package main
import (
 "fmt"
)
func main() {
 //1、传进n个参数
 sum, avg, count := GetScore(90, 82.5, 73, 64.8)
 fmt.Printf("学员共有%d⻔成绩,总成绩为:%.2f,平均成绩为:%.2f",
count, sum, avg)
 fmt.Println()
 // 2、传切⽚作为参数
 scores := []float64{92, 72.5, 93, 74.5, 89, 87, 74}
 sum, avg, count = GetScore(scores...)
 fmt.Printf("学员共有%d⻔成绩,总成绩为:%.2f,平均成绩为:%.2f",
count, sum, avg)
}
//累加求和,参数个数不定,参数个数从0-n
func GetScore(scores ...float64) (sum, avg float64, count int) {
 for _, value := range scores {
 sum += value
 count++
 }
 avg = sum / float64(count)
 return
}

4、可变参数注意细节:
⼀个函数最多只能有⼀个可变参数
参数列表中还有其它类型参数,则可变参数写在所有参数的最后
(⼋)、递归函数
1、在函数内部,可以调⽤其他函数。如果⼀个函数在内部调⽤⾃身本身,这个
函数就是递归函数。
递归函数必须满⾜以下两个条件:
1) 在每⼀次调⽤⾃⼰时,必须是(在某种意义上)更接近于解;
2) 必须有⼀个终⽌处理或计算的准则。
2、案例代码:求阶乘
计算阶乘n! = 1 x 2 x 3 x ... x n,⽤函数fact(n)表示,可以看出:fact(n) =
n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n。所以,fact(n)可以
表示为n x fact(n-1),只有n=1时需要特殊处理

package main
import "fmt"
func main() {
 fmt.Println(factorial(5))
}
//通过递归实现阶乘
func factorial(n int) int {
 if n == 0 {
 return 1
 }
 return n * factorial(n-1)
}
//通过循环实现阶乘
func getMultiple(num int) (result int) {
 result = 1
 for i:=1; i<= num; i++ {
 result *= i
 }
 return
}

3、使⽤递归的注意事项
递归的计算过程
===> factorial(5)
===> 5 * factorial(4)
===> 5 * (4 * factorial(3))
===> 5 * (4 * (3 * factorial(2)))
===> 5 * (4 * (3 * (2 * factorial(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以
⽤循环的⽅式实现,但循环的逻辑不如递归清晰。
使⽤递归函数需要注意防⽌栈溢出。在计算机中,函数调⽤是通过栈
(stack)这种数据结构实现的,每当进⼊⼀个函数调⽤,栈就会加⼀层
栈,每当函数返回,栈就会减⼀层。由于栈的⼤⼩不是⽆限的,所以,递归
调⽤的次数过多,会导致栈溢出。
使⽤递归函数的优点是逻辑简单清晰,缺点是过深的调⽤会导致栈溢出。

指针
(⼀)、指针的概述
1、指针是存储另⼀个变量的内存地址的变量。
变量是⼀种使⽤⽅便的占位符,变量都指向计算机的内存地址。
⼀个指针变量可以指向任何⼀个值的内存地址。
如下图:变量b的值为156,存储在内存地址0x1040a124。变量a持有b的
地址,则a被认为指向b。

a(0x1040a124)                                           b(156)address=- 0x1040a124

2、获取变量的地址
Go 语⾔的取地址符&,⼀个变量前使⽤&,会返回该变量的内存地址。

func main() {
 a := 10
 fmt.Printf("变量的地址: %x \n", &a)
}

运⾏结果:
变量的地址: c420014050

3、Go语⾔指针的特点:
Go语⾔指针的最⼤特点是:指针不能运算(不同于C语⾔)。
在Go语⾔中如果对指针进⾏运算会报错:nvalid operation: p++ (nonnumeric type *int)
(⼆)、声明指针
1、声明指针,*T是指针变量的类型,它指向T类型的值。
var 指针变量名 *指针类型
* 号⽤于指定变量是⼀个指针。
var ip *int //指向整型的指针
var fp *float32 //指向浮点型的指针
2、如何使⽤指针?(指针使⽤流程)
定义指针变量。
为指针变量赋值。
访问指针变量中指向地址的值。
获取指针的值:在指针类型的变量前加上 * 号(前缀)来获取指针所指向
的内容。
获取⼀个指针意味着访问指针指向的变量的值。语法是:*a
3、示例代码⼀:

func main() {
 //声明实际变量
 var a int = 120
 //声明指针变量
 var ip *int
 //给指针变量赋值,将变量a的地址赋值给ip
 ip = &a
 //打印a的类型和值
 fmt.Printf("a 的类型是%T,值是%v \n", a, a)
 //打印&a的类型和值
 fmt.Printf("&a 的类型是%T,值是%v \n", &a, &a)
 //打印ip的类型和值
 fmt.Printf("ip 的类型是%T,值是%v \n", ip, ip)
 //打印变量*ip的类型和值
 fmt.Printf("*ip 变量的类型是%T,值是%v \n", *ip, *ip)
 //打印变量*&a的类型和值
 fmt.Printf("*&a 变量的类型是%T,值是%v \n", *&a, *&a)
 fmt.Println(a, &a, *&a)
 fmt.Println(ip, &ip, *ip, *(&ip), &(*ip))
}

运⾏结果:
a 的类型是int,值是120
&a 的类型是*int,值是0xc420014050
ip 的类型是*int,值是0xc420014050
*ip 变量的类型是int,值是120
*&a 变量的类型是int,值是120
120 0xc420014050 120
0xc420014050 0xc42000c028 120 0xc420014050 0xc420014050
4、示例代码⼆:

package main
import "fmt"
type Student struct {
 name string
 age int
 married bool
 sex int8
}
func main() {
 var s1 = Student{"Steven", 35, true, 1}
 var s2 = Student{"Sunny", 20, false, 0}
 var a *Student = &s1 //将s1的内存地址赋值给Student指针变量a
 var b *Student = &s2 //将s2的内存地址赋值给Student指针变量b
 fmt.Println("\n---------------------")
 fmt.Printf("s1类型为%T,值为%v \n", s1, s1)
 fmt.Printf("s2类型为%T,值为%v \n", s2, s2)
 fmt.Println("\n---------------------")
 fmt.Printf("a类型为%T,值为%v \n", a, a)
 fmt.Printf("b类型为%T,值为%v \n", b, b)
 fmt.Println("\n---------------------")
 fmt.Printf("*a类型为%T,值为%v \n", *a, *a)
 fmt.Printf("*b类型为%T,值为%v \n", *b, *b)
 fmt.Println("\n---------------------")
 fmt.Println(s1.name, s1.age, s1.married, s1.sex)
 fmt.Println(a.name, a.age, a.married, a.sex)
 fmt.Println("\n---------------------")
 fmt.Println(s2.name, s2.age, s2.married, s2.sex)
 fmt.Println(b.name, b.age, b.married, b.sex)
 fmt.Println("\n---------------------")
 fmt.Println((*a).name, (*a).age, (*a).married, (*a).sex)
 fmt.Println((*b).name, (*b).age, (*b).married, (*b).sex)
 fmt.Println("\n---------------------")
 fmt.Printf("&a类型为%T,值为%v\n", &a, &a)
 fmt.Printf("&b类型为%T,值为%v\n", &b, &b)
 fmt.Println("\n---------------------")
 fmt.Println(&a.name, &a.age, &a.married, &a.sex)
 fmt.Println(&b.name, &b.age, &b.married, &b.sex)
}

运⾏结果:
---------------------
s1类型为main.Student,值为{Steven 35 true 1}
s2类型为main.Student,值为{Sunny 20 false 0}
---------------------
a类型为*main.Student,值为&{Steven 35 true 1}
b类型为*main.Student,值为&{Sunny 20 false 0}
---------------------
*a类型为main.Student,值为{Steven 35 true 1}
*b类型为main.Student,值为{Sunny 20 false 0}
---------------------
Steven 35 true 1
Steven 35 true 1
---------------------
Sunny 20 false 0
Sunny 20 false 0
---------------------
Steven 35 true 1
Sunny 20 false 0
---------------------
&a类型为**main.Student,值为0xc42000c028
&b类型为**main.Student,值为0xc42000c030
---------------------
0xc42000a060 0xc42000a070 0xc42000a078 0xc42000a079
0xc42000a080 0xc42000a090 0xc42000a098 0xc42000a099

(四)、空指针
1、Go 空指针
当⼀个指针被定义后没有分配到任何变量时,它的值为 nil。
nil 指针也称为空指针。
nil在概念上和其它语⾔的null、None、NULL⼀样,都指代零值或空值。
⼀个指针变量通常缩写为 ptr。
2、空指针判断:
if(ptr != nil) // ptr 不是空指针
if(ptr == nil) // ptr 是空指针
(五)、操作指针改变变量的数值

1、示例代码:

package main
import (
 "fmt"
)
func main() {
 b := 3158
 a := &b
 fmt.Println("b 的地址:", a) //0xc420014050
 fmt.Println("*a 的值:", *a) //3158
 *a++
 fmt.Println("b 的新值:", b)//3159
}

运⾏结果
b 的地址: 0xc420014050
*a 的值:3158
b 的新值: 3159
(六)、使⽤指针作为函数的参数
1、示例代码⼀(基本数据类型指针作为函数参数)

package main
import (
 "fmt"
)
func main() {
 a := 58
 fmt.Println("函数调⽤之前a的值:", a)
 fmt.Printf("%T \n", a)
 fmt.Printf("%x \n", &a)
 //b := &a
 var b *int = &a
 change(b)
 fmt.Println("函数调⽤之后的a的值:", a)
}
func change(val *int) {
 *val = 15
}

运⾏结果
函数调⽤之前a的值: 58
int
c420014050
函数调⽤之后的a的值: 15

2、示例代码⼆:

package main
import "fmt"
func main() {
 /* 定义局部变量 */
 a := 100
 b := 200
 //返回值的写法
 a, b = swap0(a, b)
 //指针作为参数的写法
 swap(&a, &b)
 fmt.Printf("交换后 a 的值 : %d\n", a)
 fmt.Printf("交换后 b 的值 : %d\n", b)
}
//具有返回值的惯⽤写法
func swap0(x, y int) (int , int) {
 return y, x
}
//指针作为参数的写法
func swap(x *int, y *int) {
 *x, *y = *y, *x
}

3、示例代码⼆:(将⼀个指向切⽚的指针传递给函数)
虽然将指针传递给⼀个切⽚作为函数的参数,可以实现对该切⽚中元素的
修改,但这并不是实现这⼀⽬标的惯⽤⽅法。惯⽤做法是使⽤切⽚。

package main
import "fmt"
func main() {
 a := [3]int{89, 90, 91}
 modify(&a)
 fmt.Println(a)
}
func modify(arr *[3]int) {
 (*arr)[0] = 189
}

运⾏结果
[189 90 91]

【备注:】建议做法是使⽤切⽚来实现更改元素数值,代码如下:

package main
import "fmt"
func main() {
 a := []int{89, 90, 91}
 modify(a[:])
 fmt.Println(a)
}
func modify(sls []int) {
 sls[0] = 190
}

(七)、指针数组
1、指针数组:就是元素为指针类型的数组。
定义⼀个指针数组,例如:var ptr [3]*string ;
有⼀个元素个数相同的数组,将该数组中每个元素的地址赋值给该指针数
组。也就是说该指针数组与某⼀个数组完全对应。
可以通过*指针变量获取到该地址所对应的数值。
2、案例代码:

package main
import "fmt"
const COUNT int = 4
func main() {
 a := [COUNT]string{"abc", "ABC", "123", "⼀⼆三"}
 i := 0
 //定义指针数组
 var ptr [COUNT]*string
 fmt.Printf("%T , %v \n", ptr, ptr)
 for i = 0; i < COUNT; i++ {
 //将数组中每个元素的地址赋值给指针数组
 ptr[i] = &a[i]
 }
 fmt.Printf("%T , %v \n", ptr, ptr)
 //获取指针数组中第⼀个值,其实就是⼀个地址
 fmt.Println(ptr[0])
 //根据数组元素的每个地址获取该地址所指向的元素的数值
 for i = 0; i < COUNT; i++ {
 fmt.Printf("a[%d] = %s \n", i, *ptr[i])
 }
}

(⼋)、指针的指针
1、如果⼀个指针变量存放的⼜是另⼀个指针变量的地址,则称这个指针变量为
指向指针的指针变量。
当定义⼀个指向指针的指针变量时,第⼀个指针存放第⼆个指针的地址,第⼆个
指针存放变量的地址:

Pointer(Address)->Pointer(Address)->Variable(Value)

2、指向指针的指针变量声明格式如下:
var ptr **int
以上指向指针的指针变量为整型。
访问指向指针的指针变量值需要使⽤两个 * 号。
3、案例代码

package main
import "fmt"
func main() {
 var a int
 var ptr *int
 var pptr **int
 a = 1234
 /* 指针 ptr 地址 */
 ptr = &a
 fmt.Println("ptr" , ptr)
 /* 指向指针 ptr 地址 */
 pptr = &ptr
 fmt.Println("pptr" , ptr)
 /* 获取 pptr 的值 */
 fmt.Printf("变量 a = %d\n", a)
 fmt.Printf("指针变量 *ptr = %d\n", *ptr)
 fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}

}
运⾏结果:
ptr 0xc420014050
pptr 0xc420014050
变量 a = 1234
指针变量 *ptr = 1234
指向指针的指针变量 **pptr = 1234

函数的参数传递
函数如果使⽤参数,该参数变量称为函数的形参。形参就像定义在函数体
内的局部变量。调⽤函数,可以通过两种⽅式来传递参数。即:值传递和引
⽤传递,或者叫做传值和传引⽤。

(⼀)、值传递(传值)
1、概念:
值传递:是指在调⽤函数时将实际参数复制⼀份传递到函数中,这样在函
数中如果对参数进⾏修改,将不会影响到原内容数据。
默认情况下,Go 语⾔使⽤的是值传递,即在调⽤过程中不会影响到原内
容数据。
每次调⽤函数,都将实参拷⻉⼀份再传递到函数中。每次拷⻉⼀份,性能
是不是就下降了呢?其实Go语⾔中使⽤指针和值传递配合就避免了性能降
低问题,也就是通过传指针参数来解决实参拷⻉的问题。

(⼆)、引⽤传递(传引⽤)
1、概念:
引⽤传递:是指在调⽤函数时将实际参数的地址传递到函数中,那么在函
数中对参数所进⾏的修改,将影响到原内容数据。
严格来说Go语⾔只有值传递⼀种传参⽅式,Go语⾔是没有引⽤传递的。
Go语⾔中可以借助传指针来实现引⽤传递的效果。函数参数使⽤指针参
数,传参时其实是在拷⻉⼀份指针参数,也就是拷⻉了⼀份变量地址。
函数的参数如果是指针,当函数调⽤时,虽然参数仍然是按拷⻉传递的,
但是此时仅仅只是拷⻉⼀个指针,也就是⼀个内存地址,这样就不⽤担⼼实
参拷⻉造成的内存浪费、时间开销、性能降低的情况。

2、引⽤传递的作⽤:
传指针使得多个函数能操作同⼀个对象。
传指针更轻量级 (8bytes),只需要传内存地址。如果参数是⾮指针参数,
那么值传递的过程中,每次在拷⻉上⾯就会花费相对较多的系统开销(内存
和时间)。所以当要传递⼤的结构体的时候,⽤指针是⼀个明智的选择。
Go语⾔中slice、map、chan类型的实现机制都是类似指针,所以可以直
接传递,⽽不必取地址后传递指针。

3、示例代码⼀:函数传传int型参数

package main
import "fmt"
/*
 ● 值传递:是指在调⽤函数时将实际参数复制⼀份传递到函数中,这样在函数
中如果对参数进⾏修改,不会影响到实际参数。
 ● 引⽤传递:是指在调⽤函数时将实际参数的地址传递到函数中,那么在函数
中对参数所进⾏的修改,将影响到实际参数。
 ● 严格来说Go语⾔只有值传递⼀种传参⽅式,Go语⾔是没有引⽤传递的。
 ● Go语⾔中可以借助传指针来实现引⽤传递的效果。函数参数使⽤指针参数,
传参时其实是在拷⻉⼀份指针参数,也就是拷⻉了⼀份变量地址。
 */
func main() {
 a := 10
 fmt.Printf("1、变量a的内存地址:%p ,值为:%v \n\n", &a, a) //10
 fmt.Printf("========int型变量a的内存地址:%p \n\n", a) //???%!p
 //传值
 changeIntVal(a)
 fmt.Printf("2、changeIntVal函数调⽤之后:变量a的内存地址:%p ,值为:
%v \n\n", &a, a) //10
 //传引⽤
 changeIntPtr(&a)
 fmt.Printf("3、changeIntPtr函数调⽤之后:变量a的内存地址:%p ,值为:
%v \n\n", &a, a) //50
}
func changeIntVal(a int) {
 fmt.Printf("--------changeIntVal函数内:值参数a的内存地址:%p ,值为:
%v \n", &a, a) //10
 a = 90
}
func changeIntPtr(a *int) {
 fmt.Printf("--------changeIntPtr函数内:指针参数a的内存地址:%p ,值
为:%v \n", &a, a) //地址
 *a = 50
}
4、示例代码⼆:函数传值和传引⽤_传slice型参数
package main
import "fmt"
func main() {
 a := []int{1, 2, 3, 4}
 fmt.Printf("1、变量a的内存地址是:%p ,值为:%v \n\n", &a, a)//[1,2,3,4]
 fmt.Printf("切⽚型变量a内存地址是:%p \n\n", a)//可以获取到地址,类似:
0xc420018080
 //传值
 changeSliceVal(a)
 fmt.Printf("2、changeSliceVal函数调⽤后:变量a的内存地址是:%p ,值
为:%v \n\n", &a, a)//[1,2,3,4]
 //传引⽤
 changeSlicePtr(&a)
 fmt.Printf("3、changeSlicePtr函数调⽤后:变量a的内存地址是:%p ,值
为:%v \n\n", &a, a)//[250,2,3,4]
}
func changeSliceVal(a []int) {
 fmt.Printf("----------changeSliceVal函数内:值参数a的内存地址是:%p ,
值为:%v \n", &a, a) //[1,2,3,4]
 fmt.Printf("----------changeSlicePtr函数内:值参数a的内存地址是:%p
\n", a)
 a[0] = 99
}
func changeSlicePtr(a *[]int) {
 fmt.Printf("----------changeSlicePtr函数内:指针参数a的内存地址是:%p
,值为:%v \n", &a, a) //&[1,2,3,4]
 (*a)[1] = 250
}
5、示例代码三:函数传值和传引⽤_传数组
package main
import "fmt"
func main() {
 a := [4]int{1, 2, 3, 4}
 fmt.Printf("1、变量a的内存地址是:%p ,值为:%v \n\n", &a, a)//[1,2,3,4]
 fmt.Printf("数组型变量a内存地址是:%p \n\n", a)//可以获取到地址? ❌
 //传值
 changeArrayVal(a)
 fmt.Printf("2、changeArrayVal函数调⽤后:变量a的内存地址是:%p ,值
为:%v \n\n", &a, a)//[99,2,3,4] ❌
 //传引⽤
 changeArrayPtr(&a)
 fmt.Printf("3、changeArrayPtr函数调⽤后:变量a的内存地址是:%p ,值
为:%v \n\n", &a, a)//[99,250,3,4] ❌
}
func changeArrayVal(a [4]int) {
 fmt.Printf("----------changeArrayVal函数内:值参数a的内存地址是:%p
,值为:%v \n", &a, a) //[1,2,3,4]
 fmt.Printf("----------changeArrayPtr函数内:值参数a的内存地址是:%p
\n", a) //获取不到地址
 a[0] = 99
}
func changeArrayPtr(a *[4]int) {
 fmt.Printf("----------changeArrayPtr函数内:指针参数a的内存地址是:
%p ,值为:%v \n", &a, a) //&[1,2,3,4]
 (*a)[1] = 250
}

6、示例代码三:函数传值和传引⽤_传struct结构体

package main
import "fmt"
type Teacher struct {
 name string
 age int
 married bool
 sex int8
}
func main() {
 a := Teacher{"Steven", 35, true, 1}
 fmt.Printf("1、变量a的内存地址是:%p ,值为:%v \n\n", &a, a)//{Steven
35 true 1}
 fmt.Printf("struct型变量a内存地址是:%p \n\n", a)//可以获取到地址?
 //传值
 changeStructVal(a)
 fmt.Printf("2、changeArrayVal函数调⽤后:变量a的内存地址是:%p ,值
为:%v \n\n", &a, a)//{Steven 35 true 1}
 //传引⽤
 changeStructPtr(&a)
 fmt.Printf("3、changeArrayPtr函数调⽤后:变量a的内存地址是:%p ,值
为:%v \n\n", &a, a)//
}
func changeStructVal(a Teacher) {
 fmt.Printf("----------changeArrayVal函数内:值参数a的内存地址是:%p
,值为:%v \n", &a, a) //
 fmt.Printf("----------changeArrayPtr函数内:值参数a的内存地址是:%p
\n", a) //获取不到地址?
 a.name = "Josh"
 a.age = 29
 a.married = false
}
func changeStructPtr(a *Teacher) {
 fmt.Printf("----------changeArrayPtr函数内:指针参数a的内存地址是:
%p ,值为:%v \n", &a, a) //{Daniel 20 false 1}
 (*a).name = "Daniel"
 (*a).age = 20
 (*a).married = false
}

(三)、值传递和引⽤传递的注意细节【重要】
1、Go语⾔中所有的传参都是值传递(传值),都是⼀个副本,⼀个拷⻉。
拷⻉的内容有时候是值类型(int、string、bool、数组、struct属于值类
型),这样就在函数中就⽆法修改原内容数据;
有的是引⽤类型(指针、slice、map、chan属于引⽤类型),这样就可以
修改原内容数据。
2、是否可以修改原内容数据,和传值、传引⽤没有必然的关系。在C++中,传
引⽤肯定是可以修改原内容数据的,在Go语⾔⾥,虽然只有传值,但是我们也
可以修改原内容数据,因为参数可以是引⽤类型。
3、传引⽤和引⽤类型是两个概念。虽然Go语⾔只有传值⼀种⽅式,但是可以通
过传引⽤类型变量达到跟传引⽤⼀样的效果。

你好:我的2025