在我们进行程序编写的时候,通常会把通用的部分,或者为了方便程序的阅读,会把功能相对单纯的部分定义到函数,增加程序的可阅读行。在计算机编程中,函数被定义为一段可以执行的代码,它接收输入并根据这些输入产生输出。函数通常将代码组织成模块化的部分,以便更容易的维护和重复使用。函数可以接收参数,这些参数可以是任何类型,如整型,浮点数等。函数也可以有返回值,返回值也可以是任何类型。函数在一个程序中可以多次被调用,根据每次调用是传入的参数,来完成不同的计算。
4.1 函数的定义
在go语言中如何定义函数呢?在go语言中定义函数使用关键字"func"来定义函数。函数在使用之前必须先定义。在前面的章节中的main函数就是使用关键字"func"定义的。在go语言中,函数有三种:普通函数、匿名函数(没有函数名称的函数)和方法(定义在struct上的函数)。在很多的面向对象的编程语言中,函数都支持重载,但是go语言中函数不允许重载。也不允许函数进行嵌套(即不能在函数内部定义函数),但是可以嵌套匿名函数。在go语言中,函数可以被理解为一个值,可以把函数赋值给一个变量,从而使这个变量也成为一个函数,函数也可以作为一个参数传递给其他的函数。函数既然可以作为参数,也可以作为另外一个函数的返回值。
在调用一个函数时,系统会给该函数分配一个新的空间(通常为栈区),编译器会通过自身的处理让这个新的空间和其它的栈的空间区分开来。调用结束时程序会销毁这个函数对应的栈空间,来释放内存。
函数的定义格式如下:
func 函数名称(参数1 类型1,参数2 类型2)返回值类型 { }
接下来会给几个例子,来进一步加深对函数的理解,代码如下:
package main
import "fmt"
// 定义一个普通函数
func sayHello(name string){
fmt.Printf("Hello, %s\n", name)
}
// 参数f是一个函数
func test(name string, f func(string)){
f(name)
}
// 定义一个有返回值的函数
func addNum(x, y int) int{
return x + y
}
// 定义一个返回值是函数的函数
func cal(s string)func(int, int) int{
switch s {
case "+":
return addNum
default:
return nil
}
}
func main(){
test("Cat", sayHello)
add := cal("+")
r := add(1, 2)
fmt.Printf("r: %v\n ", r)
}
4.2 函数的访问控制
在面向对象的编程语言中,函数通常都有访问控制符如:public,protected,private等。但是,go语言没有类似的访问控制符来控制函数的可见性,而是通过字母的大小写来控制函数的可见性。在go语言中,如果定义函数,常量,变量,类型,接口,结构等的名称是,首字母如果是大写则表示可以被其他包访问或者调用,类似于其他语言的public,非大写字母开头的只能在包内使用或者调用,类似于其他语言的private。变量也可以以"_"开头,以"_"开头的变量只能在包内被访问,属于私有变量。下面的例子可以帮助进一步理解go语言的访问控制。
package visibility
import "fmt"
const PI = 3.14 // public const, can be used anywhere
const pi = 3.14 // private const, only can be used in this pacakage
const _PI = 3.14 // private const, only can be used in this pacakage
func private_func() {
fmt.Println("This is a private function, only used in this package")
fmt.Println("Print private pi: ", pi)
fmt.Println("Print private _PI: ", _PI)
}
func Publict_func() {
fmt.Println("This is a public function, can be used in anywhere")
fmt.Println("call private function start")
private_func()
fmt.Println("call private function end")
}
package main
import (
"fmt"
"demoproject/visibility"
)
func main() {
visibility.Publict_func() // call public function
fmt.Println("Print the public const PI in visibility package: ", visibility.PI)
}
4.3 内存管理与垃圾回收
现代的编程语言内存管理分为自动管理和手动管理两种,c和c++就是手动管理内存的典型代表,在程序编写的过程中,开发人员要主动申请内存或者释放内存,这也是造成c++程序和容易产生内存异常的主要原因之一。为了减少开发者在开发过程中对内存管理的难度,go语言采用了自动内存管理,通过使用内存分配器和垃圾收集器来代替手动的内存分配与回收,因此开发着在使用go语言进行开发的时候,只需要关注业务代码,而无需关注底层的内存分配和回收。在这里简单的提一下,go语言内存分配借鉴了googole的TCMalloc的设计思想,来设计的内存分配器,具体的可以查阅TCMalloc的资料。
在早期go语言垃圾自动回收采用了标记清除(Mark-Sweep)算法,该算法是非常常见的垃圾收集算法,标记清除收集器是跟踪式垃圾收集器,其执行过程分成标记(Mark)和清除(Sweep)两个阶段:
- 标记——从根对象出发查找并标记堆中所有存活的对象;
- 清除——遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表;
随着版本的不断演进,go语言的垃圾回收机制越来越复杂,各种新的算法也不断的引入到新的垃圾回收机制中,如:三色抽象、屏障技术等,具体的垃圾回收算法可以参考go的文档。
传统的垃圾收集算法会在垃圾收集执行期间暂停应用程序的执行,一旦触发垃圾收集,垃圾收集器就会抢占CPU的使用权占据大量的计算资源以完成标记和清除工作,现在的CPU慢慢演化,垃圾收集的方式也在演进,例如增量垃圾收集和并发垃圾收集已经成为现代垃圾收集器的主要策略。
4.4 可变参数函数
在前面我们使用fmt包中的Print函数的时候,传入的参数个数是一个或者多个,这种可以传入不固定个数参数的函数就是可变参数函数。合适的使用可变参数函数,可以让代码简易易用,尤其是输入输出类函数,比如日志函数等。在go语言中定义可变参数函数,需要将函数定义为可以接受可变参数的类型,形式是:"...参数类型"。可变参数函数的,可变参数必须是函数的最后一个参数。下面就是一个简单的可变参数的例子:
package main
import "fmt"
func multity_param(description string, args ...int) {
total := 0
for _, arg := range args {
total += arg
}
fmt.Println(description, ": ", total)
}
func main() {
multity_param("Set1 Total", 1, 2, 3)
multity_param("Set2 Total", 9, 7, 5, 3, 1)
}
执行这段代码结果如下:
Set1 Total: 6
Set2 Total: 25
4.5 方法
所谓方法就是结构体的函数,因此,在对方法进行说明之前,要先说明一下什么是结构体。在go语言中,结构体就是通过自定义形成的新的类型,是类型中带有成员的一种复合类型。在go语言中没有类的概念,也不支持类的继承和面向对象的概念。在go语言中通常使用结构体和结构体成员来描述真实世界的实体和实体对应的属性。结构体的成员也可以称为"字段",访问结构体的成员变量使用".",具有以下特性:
- 字段必须拥有自己的类型和值
- 字段的名字必须唯一
- 字段的类型也可以是结构体,甚至是字段所在的结构体类型。
在go语言中的结构体定义格式如下:
type 结构体名字 struct {
字段1 类型
字段2 类型
......
}
接下来定一个Person的结构体来抽象现实生活的人,包含:姓名、性别、年龄、生日、身高等属性,结构体的定义如下:
type Person struct {
Name string
Age int
Gender string
BirthDay string
Height int
}
结构体的定义只是一种内存布局的描述,只有当结构体实例化的时候,才会正在的分配内存,因此必须在定义结构体并对其实例化后才能使用结构体的字段。结构体的实例化有以下几种方式:
- 结构体也是一种类型,可以像整型、字符串等类型一样,以var的方式声明结构体,就可以完成实例化。
var p Person p.Name = "Kevin" p.Age = 42 p.Gender = "M" p.BirthDay = "1981/01/01" p.Height = 165
- 在go语言中可以使用new关键字对类型进行实例化,结构体在实例化之后会形成指针类型的结构体。
p2 := new(Person) p2.Name = "Tom" p2.Age = 42 p2.Gender = "M" p2.BirthDay = "1981/01/01" p2.Height = 165
对于结构体,我们除了定义属性之外,还可以为结构体添加操作函数,这些添加给结构的操作函数称之为方法。被添加方法的结构体的变量名在go语言中被叫做接收器。当然,在go语言中,不仅仅结构体可以作为接收器,任何类型都可以作为接收器。为结构体添加方法的语法结构如下:
func (变量名 结构体类型) 方法名(参数 参数类型,......) 返回值 返回值类型 { }
接下来为Person定一个Print方法,实现打印Person的信息到控制台,代码如下:
func (p Person) Print() {
fmt.Println("name: ", p.Name, " Age:", p.Age, " Gender: ", p.Gender)
}
结合上面的全部实现一个完整的例子,代码如下:
package main
import "fmt"
// 定义一个Person结构体
type Person struct {
Name string
Age int
Gender string
BirthDay string
Height int
}
// 定义结构体方法Print,打印基本信息
func (p Person) Print() {
fmt.Println("name: ", p.Name, " Age:", p.Age, " Gender: ", p.Gender)
}
func main() {
// 初始化结构,并打印
var p Person
p.Name = "Kevin"
p.Age = 42
p.Gender = "M"
p.BirthDay = "1981/01/01"
p.Height = 165
p.Print()
p2 := new(Person)
p2.Name = "Tom"
p2.Age = 42
p2.Gender = "M"
p2.BirthDay = "1981/01/01"
p2.Height = 165
p2.Print()
p3 := &Person{}
p3.Name = "Windy"
p3.Age = 42
p3.Gender = "M"
p3.BirthDay = "1981/01/01"
p3.Height = 165
p3.Print()
}
4.6 回文数判断
接下来通过对一个整数是否是回文数的判断,来实践一下本章中学到的知识。所谓回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。按照这个定义,处理思路如下:
- 负数一定不是回文数
- 末位为0,且不是0的数一定不是回文数
- 如果数字的长度是奇数,则去掉中位的数字。通过判断左右两半是否相等进行回文数判断。
下面的代码就实现了上面的算法,简单的进行一个数字是否是回文数的判断,代码如下:
package main
import "fmt"
// 定义回文数结构体
type Palindrome struct {
data int
}
// 定义结构体的方法,判断是否是回文数
func (p Palindrome) isPalindrome() bool {
// 排除小于0,和最后一位为0的情况
if p.data < 0 || (p.data % 10 == 0 && p.data != 0) {
return false
}
// 定义右半的反向数字
tmp := 0
data := p.data
// 循环计算右半的反响数字和左半数字
for data > tmp {
tmp = tmp * 10 + data %10
data /= 10
}
// 如果长度是奇数,则tmp/10,舍去最后一位再进行比较
return data == tmp || data == tmp/10
}
func main() {
p := &Palindrome{}
p.data = 121
res := p.isPalindrome()
fmt.Println("121 is palindrome: ", res)
p1 := &Palindrome{}
p1.data = 123
res1 := p1.isPalindrome()
fmt.Println("123 is palindrome: ", res1)
}
执行结果如下:
121 is palindrome: true
123 is palindrome: false