文章目录
函数和包的使用
在之前的示例中,我们可以看到,基本所有代码都写在func main()当中,前面练习的参考答案中,有些还使用了函数的调用,这节会详细介绍函数和包的使用
比如我们要求多个数的阶乘,代码如下所示。
/*
M和N计算阶乘
*/
func main(){
m:=5
n:=10
fm = 1
for num:=1;num<m+1;num++{
fm *= num
}
fn = 1
for num:=1;num<n+1;num++{
fn *= num
}
}
函数的作用
不知道大家是否注意到,在上面的代码中,我们做了2次求阶乘,这样的代码实际上就是重复代码。编程大师Martin Fowler先生曾经说过:“代码有很多种坏味道,重复是最坏的一种!”,要写出高质量的代码首先要解决的就是重复代码的问题。对于上面的代码来说,我们可以将计算阶乘的功能封装到一个称之为“函数”的功能模块中,在需要计算阶乘的地方,我们只需要“调用”这个“函数”就可以了。
定义函数
在golang中可以使用func
关键字来定义函数,和变量一样每个函数也有一个响亮的名字,而且命名规则跟变量的命名规则是一致的。在函数名后面的圆括号中可以放置传递给函数的参数,圆括号后可以指定函数返回的变量类型;这一点和数学上的函数非常相似,程序中函数的参数就相当于是数学上说的函数的自变量,而函数执行完成后我们可以通过return
关键字来返回一个值,这相当于数学上说的函数的因变量。
在了解了如何定义函数后,我们可以对上面的代码进行重构,所谓重构就是在不影响代码执行结果的前提下对代码的结构进行调整,重构之后的代码如下所示。
/*
M和N计算阶乘
*/
func fat(x int) int{
sum:=1
for num:=1;num<m+1;num++{
sum *= num
}
return sum
}
func main(){
m:=5
n:=10
fm:=fat(m)
fn:=fat(n)
}
函数的参数
函数是绝大多数编程语言中都支持的一个代码的"构建块"因为我们可能会对0个或多个参数进行加法运算,而具体有多少个参数是由调用者来决定,我们作为函数的设计者对这一点是一无所知的,因此在不确定参数个数的时候,我们可以使用可变参数,golang的可变参数示例代码如下所示。
// myFunction 接受一个正常参数 normalParam(类型为 string)和一个可变参数 variadicParams(类型为 []int)。variadicParams 可以接收任意数量的整数作为输入
func myFunction(normalParam string, variadicParams ...int) {
total := 0
for _, value := range variadicParams {
total += value
}
fmt.Printf("Normal param: %s, Sum of variadic params: %d\n", normalParam, total)
}
func main(){
myFunction("Hello", 1, 2, 3, 4, 5)
}
命名返回
若在函数定义的返回值里,给返回值命名了,那么在函数中,对该命名的变量即是函数的返回值,不需要额外指定return
值,示例如下
package main
import "fmt"
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
func main() {
fmt.Println(split(17))
}
用包管理函数
对于任何一种编程语言来说,给变量、函数这样的标识符起名字都是一个让人头疼的问题,因为我们会遇到命名冲突这种尴尬的情况。golang的一个包中是不允许出现同名函数的,也不允许重载的情况出现
func foo(){
fmt.Println("hello, world!")
}
func foo(s string){
fmt.Println(s)
}
//以上情况称为重载,也就是同名函数,接受不同的输入参数,golang是不支持的,会编译报错
当出现上面的这种情况,我们需要定义了多个foo
的函数,那么怎么解决这种命名冲突呢?答案其实很简单,golang中每个文件夹就代表了一个包(package),我们在不同的包中可以有同名的函数,在使用函数的时候我们通过import
关键字导入指定的包就可以区分到底要使用的是哪个包中的Foo
函数(用大写才能被外部可见,不然是不能调用的),代码如下所示。
package1.go
package pack
import (
"fmt"
)
func Foo() {
fmt.Println("hello, world!")
}
package2.py
package pack2
import (
"fmt"
)
func Foo(s string) {
fmt.Println(s)
}
main.go
package main
import pack "golang-30-days/DayX/code/Day6/package"
import pack2 "golang-30-days/DayX/code/Day6/package2"
func main() {
s:="hello"
pack.Foo()
pack2.Foo(s)
}
在其他包中,也可以通过init
函数来用于在包被加载时自动执行初始化任务,init
函数无需手动调用,由Go运行时系统在程序启动时自动调用。具体来说,当包被导入时,其内部的所有init
函数会被顺序执行。init
函数没有参数,也不返回任何值。一个Go包中可以包含多个init
函数,按顺序执行。示例如下
package1.go
package pack
import (
"fmt"
)
func Foo() {
fmt.Println("hello, world!")
}
func init() {
fmt.Println("init finish")
}
// init是golang中的初始化任务
main.go
package main
import pack "golang-30-days/DayX/code/Day6/package"
func main() {
pack.Foo()
}
//导入package时 会执行包中init的代码
练习
练习1:实现计算求最大公约数和最小公倍数的函数。
参考答案:
func Gcd(x int, y int) int {
if x > y {
temp := x
x = y
y = temp
}
for factor := x; factor > 0; factor-- {
if x%factor == 0 && y%factor == 0 {
return factor
}
}
return 1
}
func Lcm(x int, y int) int {
return x * y
}
练习2:实现判断一个数是不是回文数的函数。
参考答案:
func Is_palindrome(x int) bool {
temp := x
total := 0
for temp > 0 {
total = total*10 + temp%10
temp /= 10
}
return total == x
}
练习3:实现判断一个数是不是素数的函数。
参考答案:
func Is_prime(num int) bool {
for factor := 0; factor < 2; factor += int(math.Pow(float64(num), 0.5)) + 1 {
if num%factor == 0 {
return false
}
}
return num != 1
}
练习4:写一个程序判断输入的正整数是不是回文素数。
参考答案:
func main() {
pack.Foo()
var num int
fmt.Scan(&num)
if pack.Is_palindrome(num) && pack.Is_prime(num) {
fmt.Sprintln("num d%是回文素数", num)
}
}
注意:通过上面的程序可以看出,当我们将代码中重复出现的和相对独立的功能抽取成函数后,我们可以组合使用这些函数来解决更为复杂的问题,这也是我们为什么要定义和使用函数的一个非常重要的原因。
变量的作用域
最后,我们来讨论一下golang中有关变量作用域的问题。
var a = 100
func foo() func() {
b := "hello"
//golang中可以在函数内部再定义函数
inerfunc := func() {
c := true
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
}
return inerfunc
}
func init() {
fmt.Println("init finish")
foo()
}
上面的代码能够顺利的执行并且打印出100、hello和true,但我们注意到了,在inerfunc
函数的内部并没有定义a
和b
两个变量,那么a
和b
是从哪里来的。我们在上面代码的函数外
中定义了一个变量a
,这是一个全局变量(global variable),属于全局作用域,因为它没有定义在任何一个函数中。在上面的foo
函数中我们定义了变量b
,这是一个定义在函数中的局部变量(local variable),属于局部作用域,在foo
函数的外部并不能访问到它;但对于foo
函数内部的inerfunc
函数来说,变量b
属于嵌套作用域,在inerfunc
函数中我们是可以访问到它的。inerfunc
函数中的变量c
属于局部作用域,在inerfunc
函数之外是无法访问的。事实上,golang查找一个变量时会按照“局部作用域”、“封闭作用域”、“全局作用域”的顺序进行搜索,我们在上面的代码中已经看到了。
再看看下面这段代码,我们希望通过函数调用修改全局变量a
的值,但实际上下面的代码是做不到的。
var a = 100
func foo() func() {
b := "hello"
//golang中可以在函数内部再定义函数
inerfunc := func() {
a := 200
c := true
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
}
return inerfunc
}
func init() {
fmt.Println("init finish")
foo()
}
在调用foo
函数后,我们发现a
的值仍然是100,这是因为当我们在函数foo
中写a := 200
的时候,是重新定义了一个名字为a
的局部变量,它跟全局作用域的a
并不是同一个变量,因为局部作用域中有了自己的变量a
,因此foo
函数不再搜索全局作用域中的a
。如果我们希望在foo
函数中修改全局作用域中的a
,代码如下所示。
var a = 100
func foo() func() {
b := "hello"
//golang中可以在函数内部再定义函数
inerfunc := func() {
a = 200
c := true
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
}
return inerfunc
}
func init() {
fmt.Println("init finish")
foo()
}
我们直接给全局变量a
赋值,就可以改变全局变量的值。
在实际开发中,我们应该尽量减少对全局变量的使用,因为全局变量的作用域和影响过于广泛,可能会发生意料之外的修改和使用,除此之外全局变量比局部变量拥有更长的生命周期,可能导致对象占用的内存长时间无法被垃圾回收。事实上,减少对全局变量的使用,也是降低代码之间耦合度的一个重要举措,同时也是对迪米特法则的践行。减少全局变量的使用就意味着我们应该尽量让变量的作用域在函数的内部,但是如果我们希望将一个局部变量的生命周期延长,使其在定义它的函数调用结束后依然可以使用它的值,这时候就需要使用闭包,闭包
在上述例子中已经有所展示,我们在后续的内容中会进行讲解。
说明: 很多人经常会将“闭包”和“匿名函数”混为一谈,但实际上它们并不是一回事,如果想了解这个概念,可以看看维基百科的解释或者知乎上对这个概念的讨论。