go语言函数详解-01

go语言函数声明

每一次函数在调用时都必须按照声明顺序为所有参数提供实参(参数值),在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。

在函数中,实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、slice(切片)、map、function、channel 等类型,实参可能会由于函数的间接引用被修改。

调用函数

函数在定义后,可以通过调用的方式,让当前代码跳转到被调用的函数中进行执行,调用前的函数局部变量都会被保存起来不会丢失,被调用的函数运行结束后,恢复到调用函数的下一行继续执行代码,之前的局部变量也能继续访问。

函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放并且失效。

go语言函数变量

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

func main() {
	var f func(int, string) (int, bool)
	f = fire
	i, b := f(11, "22")
	fmt.Println(i, b)
}
func fire(a int, b string) (int, bool) {
	fmt.Println(a, b)
	return a, true
}

go语言匿名函数

  1. 匿名函数用作回调函数
    下面的代码实现对切片的遍历操作,遍历中访问每个元素的操作使用匿名函数来实现,用户传入不同的匿名函数体可以实现对元素不同的遍历操作,代码如下:
func main() {
	visit([]int{11, 22, 33, 44}, func(v int) {
		v *= 10
		fmt.Println(v)
	})
}
func visit(s []int, f func(v int)) {
	for _, v := range s {
		f(v)
	}
}

匿名函数作为回调函数的设计在Go语言的系统包中也比较常见,strings 包中就有类似的设计,代码如下:

func TrimFunc(s string, f func(rune) bool) string {
    return TrimRightFunc(TrimLeftFunc(s, f), f)
}
使用匿名函数实现操作封装

下面这段代码将匿名函数作为 map 的键值,通过命令行参数动态调用匿名函数,代码如下:

func main() {
	cPtr := flag.String("command", "", "这是一个命令")
	flag.Parse()
	m := map[string]func() {
		"cn": func() {
			fmt.Println("中国命令")
		},
		"en": func() {
			fmt.Println("英国命令")
		},
		"han": func() {
			fmt.Println("韩国命令")
		},
	}
	f, ok := m[*cPtr]
	if ok {
		f()
	}else{
		fmt.Println("命令没找到")
	}
}

go语言函数类型实现接口

函数和其他类型一样都属于“一等公民”,其他类型能够实现接口,函数也可以,本节将对结构体与函数实现接口的过程进行对比。

首先给出本节完整的代码:

type Invoker interface {
	Call(any)
}
type Struct struct {}
func (s *Struct) Call(p any) {
	fmt.Println("from struct", p)
}
type FuncCaller func(any)
func (f FuncCaller) Call(p any) {
	f(p)
}
func main() {
	var i Invoker
	i = new(Struct)
	i.Call("abc")
	i = FuncCaller(func(p any) {
		fmt.Println("from func", p)
	})
	i.Call(55)
}

运行结果:

from struct abc
from func 55
HTTP包中的例子

HTTP 包中包含有 Handler 接口定义,代码如下:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Handler 用于定义每个 HTTP 的请求和响应的处理过程。
同时,也可以使用处理函数实现接口,定义如下:

type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

要使用闭包实现默认的 HTTP 请求处理,可以使用 http.HandleFunc() 函数,函数定义如下:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

而 DefaultServeMux 是 ServeMux 结构,拥有 HandleFunc() 方法,定义如下:

func (mux *ServeMux) HandleFunc(pattern string, handler func
(ResponseWriter, *Request)) {
    mux.Handle(pattern, HandlerFunc(handler))
}

上面代码将外部传入的函数 handler() 转为 HandlerFunc 类型,HandlerFunc 类型实现了 Handler 的 ServeHTTP 方法,底层可以同时使用各种类型来实现 Handler 接口进行处理。
函数类型类型也可以实现接口,并通过调用接口方法来实现调用函数本身,真是真实牛叉啊。
函数的声明不能直接实现接口,需要将函数定义为类型后,使用类型实现接口,当类型方法被调用时,还需要调用函数本体。

go语言闭包

go语言中的闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:
函数+引用环境=闭包
同一个函数与不同的引用环境组合,可以形成不同的实例:

一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有"记忆性",函数是编译期静态的概念,而闭包是运行期动态的概念。

在闭包内部修改引用的变量

闭包对它作用域上部的变量可以进行修改,修改引用的变量会对变量进行实际修改,通过下面的例子来理解:

func main() {
	str := "hello world"
	foo := func() {
		str = "123456"
	}
	foo()
	fmt.Println(str)  // 123456
}

在匿名函数中并没有定义str,str被定义在匿名函数之前,此时,str就被引用到了匿名函数中形成了闭包。

闭包的记忆效应

被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。

累加器的实现:

func main() {
	accumelator1 := Accumulate(1)
	fmt.Println(accumelator1())
	fmt.Println(accumelator1())
	fmt.Printf("accumelator1: %p\n", accumelator1)
	accumelator2 := Accumulate(10)
	fmt.Println(accumelator2())
	fmt.Printf("accumelator2: %p\n", accumelator2)
}
// Accumulate 提供一个值,每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
	return func() int {
		value++
		return value
	}
}
  1. 累加器生成函数,这个函数输入一个初始值,调用时返回一个为初始值创建的闭包函数
  2. 返回一个闭包函数,每次返回会创建一个新的函数实例
  3. 对引用Accumulate参数变量进行累加,注意value不是匿名函数定义的,而是被匿名函数引用,所以形成了闭包
  4. 将修改后的值通过闭包的返回值返回
  5. 对比输出的日志发现,accumulator1和accumulator2输出的函数地址不同,因此它们是两个不同的闭包实例
    每调用一次 accumulator 都会自动对引用的变量进行累加。
闭包实现生成器

闭包的记忆效应被用于实现类似于设计模式中工厂模式的生成器,下面的例子展示了创建一个玩家生成器的过程。

玩家生成器的实现:

func main() {
	// 创建一个玩家生成器
	player := PlayerGen("德玛")
	fmt.Println(player())
}
// PlayerGen 创建一个玩家生成器,输入名称,输出生成器
func PlayerGen(name string) func() (string, int) {
	hp := 150
	// 返回创建的闭包
	return func() (string, int) {
		// 将变量引用到闭包中
		return name, hp
	}
}

go语言可变参数

可变参数类型

可变参数是指函数传入的参数个数是可变的,为了做到这点,首先需要将函数定义为可以接受可变参数的类型:

func myfunc(args ...int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

形如…type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数,它是一个语法糖(syntactic sugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用,通常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的可能。
从内部实现机理上来说,类型...type本质上是一个数组切片,也就是[]type, 这也是为什么上面的参数 args 可以用 for 循环来获得每个传入的参数。
假如没有...type这样的语法糖,开发者将不得不这么写:

加入没有...type这样的语法糖,开发者将不得不这么写:

func myfunc2(args []int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

从函数的实现角度来看,这没有任何影响,该怎么写就怎么写,但从调用方来说,情形则完全不同:

myfunc2([]int{1, 3, 7, 13})

大家会发现,我们不得不加上[]int{}来构造一个数组切片实例,但是有了...type这个语法糖,我们就不用自己来处理了。

任意类型的可变参数

用interface{}传递任意类型数据是go语言的惯例用法,使用interface{}仍然是类型安全的,

func main() {
	myPrint(11, "hello", false, math.Pi)
}
func myPrint(args ...any) {
	for _, v := range args {
		switch v.(type) {
		case int:
			fmt.Println(v, "is a int")
		case string:
			fmt.Println(v, "is a string")
		case bool:
			fmt.Println(v, "is a bool")
		case float64:
			fmt.Println(v, "is a float64")
		}
	}
}
遍历可变参数列表,获取每一个参数的值
func main() {
	joinStrings("abc", "def", "jjj", "哈哈哈")
}
func joinStrings(ss ...string) {
	var b bytes.Buffer
	for _, s := range ss {
		_, _ = b.WriteString(s)
	}
	fmt.Println(b.String())
}
获得可变参数类型-获得每一个参数的类型

当可变参数为 interface{} 类型时,可以传入任何类型的值,此时,如果需要获得变量的类型,可以通过 switch 获得变量的类型,下面的代码演示将一系列不同类型的值传入printTypeValue() 函数,该函数将分别为不同的参数打印它们的值和类型的详细描述。

打印类型及值:

func main() {
	printTypeValue(15, true, "Mayanna", 3.14)
}
func printTypeValue(args ...any) {
        // 字节缓冲,作为快速字符串连接
	var b bytes.Buffer
	for _, v := range args {
                // 使用 fmt.Sprintf 配合%v动词,可以将 interface{} 格式的任意值转为字符串。
		str := fmt.Sprintf("%v", v)
		var vType string
		switch v.(type) {
		case int:
			vType = "int"
		case string:
			vType = "string"
		case float64:
			vType = "float64"
		default:
			vType = "未知"
		}
		b.WriteString("value: ")
		b.WriteString(str)
		b.WriteString(" type: ")
		b.WriteString(vType)
		b.WriteByte('\n')
	}
	fmt.Println(b.String())
}
在多个可变参数函数中传递参数
func main() {
	myPrint(11, 22, 33, 44)
}
func myPrint(args ...any) {
	rawPrint(args...)
}
func rawPrint(args ...any) {
	for _, v := range args {
		fmt.Println(v)
	}
}

可变参数使用...进行传递与切片间使用append连接是同一个特性。

posted @ 2022-08-31 10:48  专职  阅读(78)  评论(0编辑  收藏  举报