【go从入门到精通】函数详解

作者简介:

        高科先后在 IBM PlatformComputing从事网格计算,淘米网,网易从事游戏服务器开发,拥有丰富的C++,go等语言开发经验,mysql,mongo,redis等数据库,设计模式和网络库开发经验,对战棋类,回合制,moba类页游,手游有丰富的架构设计和开发经验。 (谢谢你的关注)

--------------------------------------------------------------------------------------------------------------------------------

函数定义

        Go语言函数基本组成:关键字func、函数名、参数列表、返回值、函数体和返回语句。语法如下:

func 函数名(参数列表) (返回值列表) {
    // 函数体
     return
}

        函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。

        有点简单,很多做C,C++开发的都知道声明函数原型是一件有点冗余的事情,而转到go之后,你会发现go函数用起来便轻松简洁很多,至少不需要你声明函数原型。那么我们接下来一起先看看go函数都有哪些特点:  

    • 无需声明原型。
    • 支持不定 变参。
    • 支持多返回值。
    • 支持命名返回参数。 
    • 支持匿名函数和闭包。
    • 函数也是一种类型,一个函数可以赋值给变量。

    • 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
    • 不支持 重载 (overload) 
    • 不支持 默认参数 (default parameter)。 

"func" 为定义函数的关键字,函数这一行的最末尾需要一个左大括号,注意左大括号依旧不能另起一行。

这里我写一个简单的两整数相加的函数

package main
import ("fmt"
)
func Sum(a int, b int) int {return a + b
}
func main() {fmt.Println("sum(1,2):", Sum(1, 2))}

运行下结果

sum(1,2): 3

这里实际上如果类型相同的相邻参数,我们可以吧参数类型合并一下,所以我们可以这样写Sum函数:

func Sum(a, b int) int {return a + b
}

所以接下来我们接下来要说下函数的参数

函数参数

       函数定义时有参数,该参数变量可称为函数的形参。形参就像定义在函数体内的局部变量。

但当调用函数,传递过来的变量就是函数的实参。

值传递和引用传递

函数可以通过两种方式来传递参数:

值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

func swap(x, y int) int {... ...}

引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

package mainimport ("fmt"
)/* 定义相互交换值的函数 */
func swap(x, y *int) {var temp inttemp = *x /* 保存 x 的值 */*x = *y   /* 将 y 值赋给 x */*y = temp /* 将 temp 值赋给 y*/}func main() {var a, b int = 1, 2/*调用 swap() 函数&a 指向 a 指针,a 变量的地址&b 指向 b 指针,b 变量的地址*/swap(&a, &b)fmt.Println(a, b)
}

输出结果:

    2 1

再来一个map的传参例子:

package mainimport ("fmt"
)func Mod(a []int) {for i, v := range a {a[i] = 100 + v}
}func main() {numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}Mod(numbers)fmt.Println(numbers)
}

输出结果:

[101 102 103 104 105 106 107 108 109 110]

可见,虽然我们的Mod函数传参过来并非指针,表面上好像是值传递,从结果上来看似乎又间接的修改了slice的值,因此slice   实际上是以引用的方式传递。

总结

在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。

注意2:map、slice、chan、指针、interface默认以引用的方式传递。

不定参数传值

 就是函数的参数不是固定的,后面的类型是固定的。(可变参数)

Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。

在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可。

  func myfunc(args ...int) {    //0个或多个参数}func add(a int, args…int) int {    //1个或多个参数}func add(a int, b int, args…int) int {    //2个或多个参数}

注意:其中args是一个slice,我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数.

其实说到这里你可能会想到我们最开始写helloworld的时候,用到的fmt.Println函数。接下来我用一个简单的例子来调用不定参数的函数:

package mainimport ("fmt"
)func Sum(a int, args ...int) (sum int) {sum = afor _, v := range args {sum += v}return sum
}func main() {fmt.Println("sum(1-4):", Sum(1, 2, 3, 4))
}

输出:sum(1-4): 

由于这种不定参的函数可以接受某种类型的切片 slice 为参数,因此我们的代码还可以这样调用起来:

package mainimport ("fmt"
)
func Sum(args ...int) (sum int) {sum = 0for _, v := range args {sum += v}return sum
}func main() {numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}fmt.Println("sum(1-4):", Sum(numbers ...)) // slice时使用
}

在调用的时候我们需要给slice参数后边加上...即可。

任意类型的不定参数


就是函数的参数和每个参数的类型都不是固定的。

用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。

func myfunc(args ...interface{}) {}

 函数重载

         函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参或者不同的返回值,在 Go 语言里面函数重载是不被允许的。

         当你这样写同名函数的时候如下:

func Mod(a []int) {for i, v := range a {a[i] = 100 + v}
}
func Mod(a int)  {a = a+1
}

    编译报错,信息如下:

  Mod redeclared in this block    
  other declaration of Mod

函数签名        

        函数也可以作为函数类型被使用。函数类型也就是函数签名,函数类型表示具有相同参数和结果类型的所有函数的集合。函数类型的未初始化变量的值为nil。就像下面:

type  funcType func (int, int) int

上面通过type关键字,定义了一个新类型,函数类型 funcType 。

        函数签名由函数参数、返回值以及它们的类型组成,如果两个函数的参数列表和返回值列表的变量类型能一一对应,那么这两个函数就有相同的签名,下面testa与testb具有相同的函数签名。

func testa  (a, b int, z float32) bool
func testb  (a, b int, z float32) (bool)

函数调用传入的参数必须按照参数声明的顺序。而且Go语言没有默认参数值的说法。

那么我们如何来显示的调用对应的函数呢?我们可以这样来:

package mainimport ("fmt"
)func testa(a int, b int, z float32) bool {fmt.Println("testa")return a+b > int(z)
}
func testb(a, b int, z float32) bool {fmt.Println("testb")return a+b > int(z)
}type FuncType func(int, int, float32) boolfunc main() {var z float32z = 10.345FuncType(testa)(1, 2, z)FuncType(testb)(1, 2, z)
}

函数也可以在表达式中赋值给变量,这样作为表达式中右值出现,我们称之为函数值字面量(function literal),函数值字面量是一种表达式,它的值被称为匿名函数,就像下面一样:

f := func() int { return 7 }

下面代码对以上2种情况都做了定义和调用:

package mainimport ("fmt""time"
)type funcType func(time.Time)     // 定义函数类型funcTypefunc main() {f := func(t time.Time) time.Time { return t } // 方式一:直接赋值给变量fmt.Println(f(time.Now()))var timer funcType = CurrentTime // 方式二:定义函数类型funcType变量timertimer(time.Now())funcType(CurrentTime)(time.Now())  // 先把CurrentTime函数转为funcType类型,然后传入参数调用
// 这种处理方式在Go 中比较常见
}func CurrentTime(start time.Time) {fmt.Println(start)
}

函数返回值

"_"标识符,用来忽略函数的某个返回值

Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。

返回值的名称应当具有一定的意义,可以作为文档使用。

没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。

直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。

package mainimport ("fmt"
)func add(a, b int) (c int) {c = a + breturn
}func calc(a, b int) (sum int, avg int) {sum = a + bavg = (a + b) / 2return
}func main() {var a, b int = 1, 2c := add(a, b)sum, avg := calc(a, b)fmt.Println(a, b, c, sum, avg)
}

输出结果:

    1 2 3 3 1 

Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 "_" 忽略。

package mainfunc test() (int, int) {return 1, 2
}func main() {// s := make([]int, 2)// s = test()   // Error: multiple-value test() in single-value contextx, _ := test()println(x)
}

输出结果:

    1 

多返回值可直接作为其他函数调用实参。

package mainfunc test() (int, int) {return 1, 2
}func add(x, y int) int {return x + y
}func sum(n ...int) int {var x intfor _, i := range n {x += i}return x
}func main() {println(add(test()))println(sum(test()))
}

输出结果:

    33

命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。

package mainfunc add(x, y int) (z int) {z = x + yreturn
}func main() {println(add(1, 2))
}

输出结果: 3

命名返回参数可被同名局部变量遮蔽,此时需要显式返回。

func add(x, y int) (z int) {{ // 不能在一个级别,引发 "z redeclared in this block" 错误。var z = x + y// return   // Error: z is shadowed during returnreturn z // 必须显式返回。}
}

命名返回参数允许 defer 延迟调用通过闭包读取和修改。

package mainfunc add(x, y int) (z int) {defer func() {z += 100}()z = x + yreturn
}func main() {println(add(1, 2)) 
}

输出结果:    103

显式 return 返回前,会先修改命名返回参数。

package mainfunc add(x, y int) (z int) {defer func() {println(z) // 输出: 203}()z = x + yreturn z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
}func main() {println(add(1, 2)) // 输出: 203
}

输出结果:

203

203

 函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。

package mainimport "fmt"func test(fn func() int) int {return fn()
}
// 定义函数类型。
type FormatFunc func(s string, x, y int) string func format(fn FormatFunc, s string, x, y int) string {return fn(s, x, y)
}func main() {s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。s2 := format(func(s string, x, y int) string {return fmt.Sprintf(s, x, y)}, "%d, %d", 10, 20)println(s1, s2)
}

输出结果:    100 10, 20

有返回值的函数,必须有明确的终止语句,否则会引发编译错误。

panic和recover函数简要说明

 使用场景

       Go 语言拥有一些内置函数,内置函数是预先声明的,它们像任何其他函数一样被调用,内置函数没有标准的类型,因此它们只能出现在调用表达式中,它们不能用作函数值。这里我们简要的说明下常用的panic和recover函数(细节我们将会在其他文章里专门作为一个专题来分析):

内置函数说明
panic用来表示非常严重的不可恢复的异常错误
recover用于从 panic 或 错误场景中恢复
func panic(interface{})
func recover() interface{}

panic和recover两个内置函数,协助报告和处理运行时异常和程序定义的错误。

在执行函数时,显式调用panic或者运行时发生panic都会终止函数的执行。然后,由函数延迟(defer)的任何函数都照常执行。 依此类推,直到执行goroutine中的顶级函数延迟。 此时,程序终止并报告错误条件,包括panic参数的值。

panic(42)
panic("unreachable")
panic(Error("cannot parse"))

因此当我们需要终止一个异常,我们就可以直接使用panic,比如下面的代码:

if false == LoadCfg() {panic("加载配置文件失败")
}

当进入到if条件判断里边之后,panic会将此时的堆栈信息打印输出:

这段代码里我们会看到panic的原因有输出,堆栈信息终止在InitModels函数体内,GameConfig.go的第63行,这样你能很方便的定位到问题。

recover函数用于在发生panic异常时恢复程序的控制流。它只能在defer函数中调用,并且可以捕获并处理发生的panic异常。

当程序发生panic异常时,它会中断当前的控制流程,并且开始执行所有在当前函数中定义的defer函数。在defer函数中调用recover函数,可以捕获到panic异常,并且程序可以继续正常执行。

recover函数的签名如下:

func recover() interface{}

它没有任何参数,返回一个interface{}类型的值。

当调用recover函数时,它会返回panic函数传递过来的值,如果没有发生panic异常或者recover函数不是在defer函数中调用,那么它会返回nil。

通常情况下,我们会在defer函数中使用recover函数来捕获和处理panic异常。示例代码如下:

func main() {defer func() {if r := recover(); r != nil {fmt.Println("Recovered:", r)}}()panic("Something went wrong!")
}

在上面的示例中,我们使用defer函数在程序发生panic异常时调用recover函数。在defer函数中,我们通过检查recover函数的返回值是否为nil来判断是否发生了panic异常,并进行相应的处理。

需要注意的是,recover函数只能在defer函数中调用,否则它不会起作用。另外,一旦recover函数被调用并且成功恢复了程序的控制流,后续的defer函数中的代码将不会执行。

和try-catch的区别

实际上有很多人在练习刚才的例子的时候很容易和try catch联系起来(如果你有其他语言开发基础),作为初学者的时候,我也会这样联想。的确,它与其他语言中的 try-catch 机制相似recover 充当了异常处理器的角色,可以在出现异常的函数中使用 defer 语句来捕获和处理 panic 异常。

然而,需要注意的是,在 Go 语言中的 panic-recover 机制与传统的 try-catch 不完全一样。在 Go 中,panic 是一种特殊的异常情况,它表示发生了不可恢复的错误。当 panic 异常发生时,程序会终止当前的执行流程,并从调用栈中逐层寻找 defer 函数,直到找到 recover 函数为止。

与 try-catch 不同的是,Go 中的 recover 函数只能在 defer 函数中使用,并且只能在同一个 goroutine 中的调用中生效。这意味着 recover 并不能用来处理跨协程的异常。另外,由于 recover 只能在 defer 中使用,它也不能用来捕获和处理其他非 panic 的异常。

总结来说,Go 语言中的 recover 机制可以起到类似于 try-catch 的异常处理作用,但有一些细节上的差异。在使用 recover 时需要注意其在 defer 函数中的限制和局限性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/773891.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【蓝桥杯】tarjan算法

一.概述 Tarjan 算法是基于DFS的算法,用于求解图的连通性问题。 Tarjan 算法可以在线性时间内求出: 无向图: 割点与桥双连通分量 有向图: 强连通分量必经点与必经边 1.割点: 若从图中删除节点 x 以及所有与 x 关联的…

Intel AIPC发布会:开启AI终端应用的新纪元

2024年3月27日下午,Intel在北京市朝阳区凤凰中心举办了AIPC发布会开启了AI终端应用的新征程。 整场发布会围绕着‘让不可想象,变为寻常’主线进行。在本次发布会上,众多PC端的AI应用得到了展示,包括:智谱AI&#xff…

第十一届蓝桥杯大赛第二场省赛试题 CC++ 研究生组-寻找2020

数据很恶心&#xff0c;但是考点挺友好~ 把测试数据黏贴到记事本中&#xff0c;知测试数据的行列数 然后根据规则判断2020是否出现&#xff0c;并累计其次数即可。 判断可能需要注意超出下标&#xff0c;可以索性把数组定大些。 #include<stdio.h> const int N 310; ch…

哈曼卡顿音箱解决关闭自动休眠 + 自用车载音乐分享制作

一&#xff1a;哈曼卡顿音箱解决关闭自动休眠 1. 背景&#xff1a;每天做最多的事情就是开音箱电源。问了客服&#xff0c;说只有玻璃4才能关闭休眠。搞得我都想买新音箱了。 2. 解决办法&#xff1a;电脑开机启动一个阻止功放休眠.exe&#xff0c;可以设置自动启动&#x…

Redis入门到实战-第十九弹

Redis实战热身Count-min-sketch篇 完整命令参考官网 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不一定100%复现, 还要以官方信息为准 https://redis.io/Redis概述 Redis是一个开源的&#xff08;采用BSD许可证&#xff09;&#xff0c;用作数据库、缓存、…

Android Studio 无法下载 gradle-7.3.3-bin.zip

下载新的Android Studio&#xff0c;然后创建新的工程时&#xff0c;出现报错&#xff1a;Could not install Gradle distribution from https://services.gradle.org/distributions/gradle-7.3.3-bin.zip 或者超时&#xff0c;我们可以复制&#xff1a;https://services.grad…

IntellIJ Idea 内存不足时怎么设置

文章目录 前言背景一、 内存显示二、 在IDEA中设置内存三 、在IDEA中打开内存的设置文件四、 JetBrains ToolBox 中安装 IntellIJ Idea配置文件位置总结 前言 请各大网友尊重本人原创知识分享&#xff0c;谨记本人博客&#xff1a;南国以南i、 提示&#xff1a;以下是本篇文章…

【React】react 使用 lazy 懒加载模式的组件写法,外面需要套一层 Loading 的提示加载组件

react 组件按需加载问题解决 1 错误信息2 解决方案 1 错误信息 react 项目在创建 router 路由时&#xff0c;使用 lazy 懒加载时&#xff0c;导致以下报错&#xff1a; The above error occurred in the <Route.Provider> component:Uncaught Error: A component suspe…

计算机基础系列 —— CPU

“Make everything as simple as possible, but no simpler.” – Albert Einstein 文中提到的所有实现都可以参考&#xff1a;nand2tetris_sol&#xff0c;但是最好还是自己学习课程实现一遍&#xff0c;理解更深刻。 之前的文章里我们介绍了 Register、PC、RAM 和 ALU&#…

【Linux实践室】Linux用户管理实战指南:用户密码管理操作详解

&#x1f308;个人主页&#xff1a;聆风吟_ &#x1f525;系列专栏&#xff1a;Linux实践室、网络奇遇记 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 一. ⛳️任务描述二. ⛳️相关知识2.1 &#x1f514;用户密码存放地及方式2.2 &#x1f514;使用…

游戏本续航@控制中心的省电模式效果如何

文章目录 节能模式长续航模式&#x1f47a;相关工具 节能模式长续航模式&#x1f47a; 蓝天模具Control Center中的模式 根据我的试验,以及软件的提示,可以发现 Power Saving是最省电的,儿Quiet模式并不省电,它会启用独立显卡,只不过风扇的转速不像娱乐模式和性能模式那么积极而…

UE5学习日记——蓝图节点前缀关键字整理

一、起因 节点如海&#xff0c;中英文翻译的时候还是有差别的&#xff0c;比如&#xff1a; 同一个中文&#xff0c;可能在英文里完全不同&#xff0c;连出现位置可能都不一样 附加 Attach Actor To Component&#xff08;将Actor附加到组件&#xff09;Append Array&#xf…

数据分析和机器学习库Pandas的使用

Pandas 库是一个免费、开源的第三方 Python 库&#xff0c;是 Python 数据分析和机器学习的工具之一。Pandas 提供了两种数据结构&#xff0c;分别是 Series&#xff08;一维数组结构&#xff09;与 DataFrame&#xff08;二维数组结构&#xff09;&#xff0c;极大地增强的了 …

Web API —— DOM 学习(四)(完结)

目录 一、日期对象 &#xff08;一&#xff09;实例化 &#xff08;二&#xff09;日期对象方法 1.时间戳介绍 2.获得时间戳的方式 getTime()方法 new Date()方法 Date.now()方法 二、节点操作 &#xff08;一&#xff09;DOM 节点 1.节点类型 元素节点 &#xff08…

1+x中级题目练习复盘(20220625 1+X 中级理论考试)

Override 用于标注重写方法 函数式接口是指有且只有一个抽象方法的接口&#xff1b;

vue项目使用eletron将打包成桌面应用(.exe)

vue项目使用eletron将打包成桌面应用(.exe) 1.前期准备 两个项目&#xff1a; 1、自己用vue cli创建的项目 2、第二个是去gitee将案例clone下来 案例地址 https://gitee.com/qingplus/electron-quick-start.git 2、测试案例是否可以正常运行 # 进入项目 cd electron-quick-…

任务管理工具Trello体验如何?一文揭秘

Trello是一款高效的协作与工作管理应用&#xff0c;这里我们将详细介绍Trello的功能、特点、优劣势、价格、定价、发展历程、使用场景以及使用技巧等等。 一、Trello 是什么 Trello是一款高效的协作与工作管理应用&#xff0c;设计用于跟踪团队项目、凸显当前活动任务、指派责…

POJ3037 + HDU-6714

两道最短路好题 POJ3037 手玩一下 发现每一点的速度可以直接搞出来&#xff0c;就是pow(2,h[1][1]-h[i][j])*V 那么从这个点出发到达别的点的耗费的时间都是上面这个数的倒数&#xff0c;然后直接跑最短路就好了 #include<iostream> #include<vector> #include<…

赛氪网积极参与千校万企协同创新行动,推动产学研深度融合

3月17日&#xff0c;中国千校万企协同创新推进会在北京盛大召开&#xff0c;会议旨在实现高校与行业龙头企业技术升级需求的精准对接&#xff0c;加速新质生产力的形成与发展。教育部党组成员、副部长孙尧&#xff0c;科技部成果转化司副司长秦浩源&#xff0c;国家知识产权局知…

王者荣耀国服米莱迪 - 出装打法详细教学视频(最新)

01. 恰到好处的一技能 02. 灵魂二技能 03. 无敌大招连招 04. 为所欲为的出装 05. 拥有这个召唤师技能赢一半 06. 英雄克制 07. 整局精讲 08. 米莱迪最强出装 09. 米莱迪最强出装的铭文 发送内容: "米莱迪", 获取提取码