Go中函数特性简介

对Go中的函数特性做一个总结。懂则看,不懂则算。

  1. Go中有3种函数:普通函数、匿名函数(没有名称的函数)、方法(定义在struct上的函数)。
  2. Go编译时不在乎函数的定义位置,但建议init()定义在最前面(如果有的话),main函数定义在init()之后,然后再根据函数名的字母顺序或者根据调用顺序放置各函数的位置。
  3. 函数的参数、返回值以及它们的类型,结合起来成为函数的签名(signature)。
  4. 函数调用的时候,如果有参数传递给函数,则先拷贝参数的副本,再将副本传递给函数。
    • 由于引用类型(slice、map、interface、channel)自身就是指针,所以这些类型的值拷贝给函数参数,函数内部的参数仍然指向它们的底层数据结构。
  5. 函数参数可以没有名称,例如func myfunc(int,int)
  6. Go中的函数可以作为一种type类型,例如type myfunc func(int,int) int
    • 实际上,在Go中,函数本身就是一种类型,它的signature就是所谓的type,例如func(int,int) int。所以,当函数ab()赋值给一个变量ref_abref_ab := ab,不能再将其它函数类型的函数cd()赋值给变量ref_ab
  7. Go中作用域是词法作用域,意味着函数的定义位置决定了它能看见的变量。
  8. Go中不允许函数重载(overload),也就是说不允许函数同名。
  9. Go中的函数不能嵌套函数,但可以嵌套匿名函数。
  10. Go实现了一级函数(first-class functions),Go中的函数是高阶函数(high-order functions)。这意味着:
    • 函数是一个值,可以将函数赋值给变量,使得这个变量也成为函数
    • 函数可以作为参数传递给另一个函数
    • 函数的返回值可以是一个函数
    • 这些特性使得函数变得无比的灵活,例如回调函数、闭包等等功能都依赖于这些特性。
  11. Go中的函数不支持泛型(目前不支持),但如果需要泛型的情况,大多数时候都可以通过接口、type switch、reflection的方式来解决。但使用这些技术使得代码变得更复杂,性能更低。

参数和返回值

函数可以有0或多个参数,0或多个返回值,参数和返回值都需要指定数据类型,返回值通过return关键字来指定。

return可以有参数,也可以没有参数,这些返回值可以有名称,也可以没有名称。Go中的函数可以有多个返回值。

  • (1).当返回值有多个时,这些返回值必须使用括号包围,逗号分隔
  • (2).return关键字中指定了参数时,返回值可以不用名称。如果return省略参数,则返回值部分必须带名称
  • (3).当返回值有名称时,必须使用括号包围,逗号分隔,即使只有一个返回值
  • (4).但即使返回值命名了,return中也可以强制指定其它返回值的名称,也就是说return的优先级更高
  • (5).命名的返回值是预先声明好的,在函数内部可以直接使用,无需再次声明。命名返回值的名称不能和函数参数名称相同,否则报错提示变量重复定义
  • (6).return中可以有表达式,但不能出现赋值表达式,这和其它语言可能有所不同。例如return a+b是正确的,但return c=a+b是错误的

例如:

// 单个返回值
func func_a() int{
return a
} // 只要命名了返回值,必须括号包围
func func_b() (a int){
// 变量a int已存在,无需再次声明
a = 10
return
// 等价于:return a
} // 多个返回值,且在return中指定返回的内容
func func_c() (int,int){
return a,b
} // 多个返回值
func func_d() (a,b int){
return
// 等价于:return a,b
} // return覆盖命名返回值
func func_e() (a,b int){
return x,y
}

Go中经常会使用其中一个返回值作为函数是否执行成功、是否有错误信息的判断条件。例如return value,existsreturn value,okreturn value,err等。

当函数的返回值过多时,例如有4个以上的返回值,应该将这些返回值收集到容器中,然后以返回容器的方式去返回。例如,同类型的返回值可以放进slice中,不同类型的返回值可以放进map中。

但函数有多个返回值时,如果其中某个或某几个返回值不想使用,可以通过下划线_这个blank identifier来丢弃这些返回值。例如下面的func_a函数两个返回值,调用该函数时,丢弃了第二个返回值b,只保留了第一个返回值a赋值给了变量a

func func_a() (a,b int){
return
} func main() {
a,_ := func_a()
}

按值传参

Go中是通过传值的方式传参的,意味着传递给函数的是拷贝后的副本,所以函数内部访问、修改的也是这个副本。

例如:

a,b := 10,20
min(a,b)
func min(x,y int) int{}

上面调用min()时,是将a和b的值拷贝一份,然后将拷贝的副本赋值给变量x,y的,所以min()函数内部,访问、修改的一直是a、b的副本,和原始的数据对象a、b没有任何关系。

如果想要修改外部数据(即上面的a、b),需要传递指针。

例如,下面两个函数,func_value()是传值函数,func_ptr()是传指针函数,它们都修改同一个变量的值。

package main

import "fmt"

func main() {
a := 10
func_value(a)
fmt.Println(a) // 输出的值仍然是10 b := &a
func_ptr(b)
fmt.Println(*b) // 输出修改后的值:11
} func func_value(x int) int{
x = x + 1
return x
} func func_ptr(x *int) int{
*x = *x + 1
return *x
}

map、slice、interface、channel这些数据类型本身就是指针类型的,所以就算是拷贝传值也是拷贝的指针,拷贝后的参数仍然指向底层数据结构,所以修改它们可能会影响外部数据结构的值。

另外注意,赋值操作b = a+1这种类型的赋值也是拷贝赋值。换句话说,现在底层已经有两个数据对象,一个是a,一个是b。但a = a+1这种类型的赋值虽然本质上是拷贝赋值,但因为a的指针指向特性,使得结果上看是原地修改数据对象而非生成新数据对象。

变长参数"..."(variadic)

有时候参数过多,或者想要让函数处理任意多个的参数,可以在函数定义语句的参数部分使用ARGS...TYPE的方式。这时会将...代表的参数全部保存到一个名为ARGS的slice中,注意这些参数的数据类型都是TYPE。

...在Go中称为variadic,在使用...的时候(如传递、赋值),可以将它看作是一个slice,下面的几个例子可以说明它的用法。

例如:func myfunc(a,b int,args...int) int {}。除了前两个参数a和b外,其它的参数全都保存到名为args的slice中,且这些参数全都是int类型。所以,在函数内部就已经有了一个args = []int{....}的数据结构。

例如,下面的例子中,min()函数要从所有参数中找出最小的值。为了实验效果,特地将前两个参数a和b独立到slice的外面。min()函数内部同时会输出保存到args中的参数值。

package main

import "fmt"

func main() {
a,b,c,d,e,f := 10,20,30,40,50,60
fmt.Println(min(a,b,c,d,e,f))
} func min(a,b int,args...int) int{
// 输出args中保存的参数
// 等价于 args := []int{30,40,50,60}
for index,value := range args {
fmt.Printf("%s%d%s %d\n","args[",index,"]:",value)
} // 取出a、b中较小者
min_value := a
if a>b {
min_value = b
}
// 取出所有参数中最小值
for _,value := range args{
if min_value > value {
min_value = value
}
}
return min_value
}

但上面代码中调用函数时传递参数的方式显然比较笨重。如果要传递的参数过多(要比较的值很多),可以先将这些参数保存到一个slice中,再传递slice给min()函数。传递slice给函数的时候,使用SLICE...的方式即可。

func main() {
s1 := []int{30,40,50,60,70}
fmt.Println(min(10,20,s1...))
}

上面的赋值方式已经能说明能使用slice来理解...的行为。另外,下面的例子也能很好的解释:

// 声明f1()
func f1(s...string){
// 调用f2()和f3()
f2(s...)
f3(s)
} // 声明f2()和f3()
func f2(s...string){}
func f3(s []string){}

如果各参数的类型不同,又想定义成变长参数,该如何?第一种方式,可以使用struct,第二种方式可以使用接口。接口暂且不说,如果使用struct,大概如下:

type args struct {
arg1 string
arg2 int
arg3 type3
}

然后可以将args传递给函数:f(a,b int,args{}),如果args结构中需要初始化,则f(a,b int,args{arg1:"hello",arg2:22})

内置函数

在builtin包中有一些内置函数,这些内置函数额外的导入包就能使用。

有以下内置函数:

$ go doc builtin | grep func
func close(c chan<- Type)
func delete(m map[Type]Type1, key Type)
func panic(v interface{})
func print(args ...Type)
func println(args ...Type)
func recover() interface{}
func complex(r, i FloatType) ComplexType
func imag(c ComplexType) FloatType
func real(c ComplexType) FloatType
func append(slice []Type, elems ...Type) []Type
func make(t Type, size ...IntegerType) Type
func new(Type) *Type
func cap(v Type) int
func copy(dst, src []Type) int
func len(v Type) int
  • close用于关闭channel
  • delete用于删除map中的元素
  • copy用于拷贝slice
  • append用于追加slice
  • cap用于获取slice的容量
  • len用于获取
    • slice的长度
    • map的元素个数
    • array的元素个数
    • 指向array的指针时,获取array的长度
    • string的字节数
    • channel的channel buffer中的未读队列长度
  • printprintln:底层的输出函数,用来调试用。在实际程序中,应该使用fmt中的print类函数
  • compleximagreal:操作复数(虚数)
  • panicrecover:处理错误
  • newmake:分配内存并初始化
    • new适用于为值类(value type)的数据类型(如array,int等)和struct类型的对象分配内存并初始化,并返回它们的指针给变量。如v := new(int)
    • make适用于为内置的引用类的类型(如slice、map、channel等)分配内存并初始化底层数据结构,并返回它们的指针给变量,同时可能会做一些额外的操作

注意,地址和指针是不同的。地址就是数据对象在内存中的地址,指针则是占用一个机器字长(32位机器是4字节,64位机器是8字节)的数据,这个数据中存储的是它所指向数据对象的地址。

a -> AAAA
b -> Pointer -> BBBB

new()和make()构造数据对象赋值给变量的都是指向数据对象的指针。

递归函数

函数内部调用函数自身的函数称为递归函数。

使用递归函数最重要的三点:

  1. 必须先定义函数的退出条件,退出条件基本上都使用退出点来定义,退出点常常也称为递归的基点,是递归函数的最后一次递归点,或者说没有东西可递归时就是退出点。
  2. 递归函数很可能会产生一大堆的goroutine(其它编程语言则是出现一大堆的线程、进程),也很可能会出现栈空间内存溢出问题。在其它编程语言可能只能设置最大递归深度或改写递归函数来解决这个问题,在Go中可以使用channel+goroutine设计的"lazy evaluation"来解决。
  3. 递归函数通常可以使用level级数的方式进行改写,使其不再是递归函数,这样就不会有第2点的问题。

例如,递归最常见的示例,求一个给定整数的阶乘。因为阶乘的公式为n*(n-1)*...*3*2*1,它在参数为1的时候退出函数,也就是说它的递归基点是1,所以对是否为基点进行判断,然后再写递归表达式。

package main

import "fmt"

func main() {
fmt.Println(a(5))
} func a(n int) int{
// 判断退出点
if n == 1 {
return 1
}
// 递归表达式
return n * a(n-1)
}

它的调用过程大概是这样的:

再比如斐波那契数列,它的计算公式为f(n)=f(n-1)+f(n-2)f(2)=f(1)=1。它在参数为1和2的时候退出函数,所以它的退出点为1和2。

package main

import "fmt"

func main() {
fmt.Println(f(3))
} func f(n int) int{
// 退出点判断
if n == 1 || n == 2 {
return 1
}
// 递归表达式
return f(n-1)+f(n-2)
}

如何递归一个目录?它的递归基点是文件,只要是文件就返回,只要是目录就进入。所以,伪代码如下:

func recur(dir FILE) FILE{
// 退出点判断
if (dir is a file){
return dir
} // 当前目录的文件列表
file_slice := filelist() // 遍历所有文件
for _,file := range file_slice {
return recur(file)
}
}

匿名函数

匿名函数是没有名称的函数。一般匿名函数嵌套在函数内部,或者赋值给一个变量,或者作为一个表达式。

定义的方式:

// 声明匿名函数
func(args){
...CODE...
} // 声明匿名函数并直接执行
func(args){
...CODE...
}(parameters)

下面的示例中,先定义了匿名函数,将其赋值给了一个变量,然后在需要的地方再去调用执行它。

package main

import "fmt"

func main() {
// 匿名函数赋值给变量
a := func() {
fmt.Println("hello world")
}
// 调用匿名函数
a()
fmt.Printf("%T\n", a) // a的type类型:func()
fmt.Println(a) // 函数的地址
}

如果给匿名函数的定义语句后面加上(),表示声明这个匿名函数的同时并执行:

func main() {
msg := "Hello World"
func(m string) {
fmt.Println(m)
}(msg)
}

其中func(c string)表示匿名函数的参数,func(m string){}(msg)msg表示传递msg变量给匿名函数,并执行。

func type

可以将func作为一种type,以后可以直接使用这个type来定义函数。

package main

import "fmt"

type add func(a,b int) int

func main() {
var a add = func(a,b int) int{
return a+b
}
s := a(3,5)
fmt.Println(s)
}

Go基础系列:函数(1)的更多相关文章

  1. Python基础系列----函数,面向对象,异常

    1.前言                                                                                               前 ...

  2. javascript基础系列(入门前须知)

    -----------------------小历史---------------------------- javascript与java是两种语言,他们的创作公司不同,JavaScript当时是借 ...

  3. PHP 基础系列(三) 【转】PHP 函数实现原理及性能分析

    作者:HDK (百度) 前言 在任何语言中,函数都是最基本的组成单元.对于PHP的函数,它具有哪些特点?函数调用是怎么实现的?php函数的性能如何,有什么使用建议?本文将从原理出发进行分析结合实际的性 ...

  4. 【Basics of Entity Framework】【EF基础系列1】

    EF自己包括看视频,看MSDN零零散散的学了一点皮毛,这次打算系统学习一下EF.我将会使用VS2012来学习这个EF基础系列. 现在看看EF的历史吧: EF版本 相关版本特性介绍 EF3.5 基于数据 ...

  5. C#基础系列——委托实现简单设计模式

    前言:上一篇介绍了下多线程的相关知识:C#基础系列——多线程的常见用法详解,里面就提到了委托变量.这篇简单介绍下委托的使用.当然啦,园子里面很多介绍委托的文章都会说道:委托和事件的概念就像一道坎,过了 ...

  6. C#基础系列——再也不用担心面试官问我“事件”了

    前言:作为.Net攻城狮,你面试过程中是否遇到过这样的问题呢:什么是事件?事件和委托的区别?既然事件作为一种特殊的委托,那么它的优势如何体现?诸如此类...你是否也曾经被问到过?你又是否都答出来了呢? ...

  7. C#基础系列:开发自己的窗体设计器(PropertyGrid显示中文属性名)

    既然是一个窗体设计器,那就应该能够设置控件的属性,设置属性最好的当然是PropertyGrid了,我们仅仅需要使用一个PropertyGrid.SelectedObject = Control就可以搞 ...

  8. C#基础系列:实现自己的ORM(反射以及Attribute在ORM中的应用)

    反射以及Attribute在ORM中的应用 一. 反射什么是反射?简单点吧,反射就是在运行时动态获取对象信息的方法,比如运行时知道对象有哪些属性,方法,委托等等等等.反射有什么用呢?反射不但让你在运行 ...

  9. linux tricks 之VA系列函数.

    VA函数(variable argument function),参数个数可变函数,又称可变参数函数.C/C++编程中,系统提供给编程人员的va函数很少.*printf()/*scanf()系列函数, ...

  10. 【C++自我精讲】基础系列二 const

    [C++自我精讲]基础系列二 const 0 前言 分三部分:const用法.const和#define比较.const作用. 1 const用法 const常量:const可以用来定义常量,不可改变 ...

随机推荐

  1. 学以致用三十一-----IPAddressField has been removed

    python 和 django版本 在进行makemigrations的时候报错 设置的字段 class Servers(models.Model): '''服务器信息''' hostname = m ...

  2. Day05 (黑客成长日记) 文件操作系列

    文件操作: 1.以什么编码方式输出,就以什么编码方式打开 f = open('d:\文件操作.txt',mode='r',encoding='GB2312') G = f.read() print(G ...

  3. fiddler抓包工具总结

    Fiddler 抓包工具总结 Fiddler是一个蛮好用的抓包工具,可以将网络传输发送与接受的数据包进行截获.重发.编辑.转存等操作.也可以用来检测网络安全.反正好处多多,举之不尽呀!当年学习的时候也 ...

  4. 模板层template

    继续之前的views,你可 能已经注意到我们例子中视图中返回的的方式有点特别.也就是说.HTML被直接硬编码在Python代码之中 def current_datetime(request): now ...

  5. [f]聊天的时间格式化

    代码如下: 参数: t: 时间戳, type:返回格式(1:IM界面,其他:会话列表) function formartTime(t, type) { var oldtime = new Date(t ...

  6. Catalog

      Java SE EE| Hibernate | Struts2Spring/SpringMVC | MyBatis C# Python PHP C/C++ | STL 汇编语言           ...

  7. Vim常用的命令

    Noted:均在命令模式下进行的 移动: j---->向下 k---->向上 l---->向右 h---->向左 保存: w---->保存 退出: q---->退出 ...

  8. Delegate、Thread、Task、ThreadPool几种方式创建异步任务性能对比

    开始预测的结果是 Task>Delegate>ThreadPool>>Thread. (一)测试代码 static async Task<int> AsyncTas ...

  9. LAMP安装教程

    LAMP环境配置安装注意安装步骤及说明事项. Linux + apache+mysql+php 附件: 1. 访问ftp报错 解决: 关闭selinux vi  /etc/selinux/config ...

  10. 深入Java集合学习系列:LinkedHashMap的实现原理

    参考下面链接: http://zhangshixi.iteye.com/blog/673789