Go 快速开发 | 10 - Go 中的函数和接口

107 阅读6分钟

一、函数

函数的定义

Go 中的函数与其他语言中的函数的作用一致,都是可重复使用的、用于执行指定操作的代码,Go 中函数的定义需要使用到 func 关键字,并且使用 return 关键字来返回一个或者多个值

func 函数名(参数1 参数类型, 参数2 类型) 返回值类型 {
    // 代码块
}

如果函数名的首字母大写就表示这是一个可以被外部调用的函数,如果是小写则表明是私有函数。

func main(){

   // 定义函数就是定义一块内存地址,打印函数名会直接输出内存地址
   fmt.Println(sayHello)

   // 通过函数名调用函数
   sayHello()

}


// 定义一个无参,无返回值的函数
func sayHello(){
   fmt.Println("Hello Go!")
}

执行上述代码,输出结果如下:

0x1089840
Hello Go!

Go 的函数支持多个返回多个值,多个返回值必须使用括号包裹起来。

func main(){

   res, err := getDivide(4,2)
   fmt.Println(res, err)

   res1, err1 := getDivide(4,0)
   fmt.Println(res1, err1)

}


func getDivide(x, y int) (int, string) {
   if y == 0 {
      return -1, "除数不能为0"
   }
   return x / y, ""
}

执行上述代码,输出结果如下:

2 
-1 除数不能为0

函数返回多个值,调用的时候要使用多个变量来接收。函数参数中如果参数的类型相同可以一起定义,累心个不同则分开定义,并使用逗号隔开。

变量作用域

全局变量是定义在函数外部的变量,它在整个程序运行周期内都有效,并且在函数中可以访问到全局变量。

var yankee = "25"


func main(){
   
   outputGlobalVar()

}

func outputGlobalVar(){
   fmt.Println("打印全局变量 yankee 的值", yankee)
}

执行上述代码,输出结果如下:

打印全局变量 yankee 的值 25

全局变量不能使用 := 形式定义,只有局部变量才可以使用这种形式定义。

Go 语言程序执行时,首先会执行定义全局变量的代码,接着执行 init 函数,然后执行 main 函数。

在函数(包括 main 函数)或代码块(for循环代码块)中定义的变量为局部变量,函数内定义的变量只在函数内有效,无法在函数外被调用。

func main(){
   printAreaVar()
   // fmt.Println(xray)
}

func printAreaVar(){
   xray := "这是一个局部变量"
   fmt.Println(xray)
}

如果在定义变量的代码块之外使用变量,则会报错 “undefined”

当全局变量和局部变量拥有相同的变量名时,优先使用局部变量,如果局部变量没有则使用全局变量。

var whiskey = "GlobalVar"

func main(){

   whiskey := "AreaVar"
   // 优先使用局部定义的 whiskey 变量
   fmt.Println("优先使用局部的 whiskey 变量",whiskey)
   // 修改的也是局部的 whiskey 变量
   whiskey = "UpdatedVar"
   fmt.Println("局部变量修改成功", whiskey)

   // 打印和修改全局的 whiskey 变量
   printVar()

}

func printVar(){
   // 函数体内没有 whiskey 变量,使用全局的 whiskey 变量
   fmt.Println(whiskey)
   // 修改全局的 whiskey 变量
   whiskey = "UpdatedByFuncVal"
   fmt.Println(whiskey)
}

执行上述代码,输出结果如下:

优先使用局部的 whiskey 变量 AreaVar
局部变量修改成功 UpdatedVar
GlobalVar
UpdatedByFuncVal

当函数的参数传入一个变量时,该变量使用的是函数被调用代码块中的局部变量,如果没有则使用局部变量。

var whiskey = "GlobalVar"

func main(){

   whiskey := "AreaVar"
   // 优先使用局部定义的 whiskey 变量
   fmt.Println("优先使用局部的 whiskey 变量",whiskey)

   // 打印和修改全局的 whiskey 变量
   printVar(whiskey)
   fmt.Println(whiskey)

}

func printVar(whiskey string){
   
   fmt.Println(whiskey)
   whiskey = "UpdatedByFuncVal"
   fmt.Println(whiskey)
}

执行上述代码,输出结果如下:

优先使用局部的 whiskey 变量 AreaVar
AreaVar
UpdatedByFuncVal
AreaVar

从输出结果来看,局部变量 whiskey 只在 printVar 函数内部被修改成功,在 main 函数中仍然保持不变,这是因为给 printVar 函数传参的时候传递的是 局部变量 whiskey 的拷贝的副本,所以对原始的局部变量 whiskey 没有影响。

二、Go 的接口

接口的定义

Go 语言中的接口是一种类型,接口只定义规范,不定义实现。

接口的定义会使用到 interface 关键字

type 接口名 interface {
    // 函数名,不定义实现
}
func main() {
   // 实例化一个结构体对象 
   human := Human{
      Name: "tony",
      Age:  18,
   }

   var walker Walker
   walker = human
   // 通过接口变量调用行为函数
   walker.walk()
}

// 定义一个接口
type Walker interface {
   walk()
}

type Human struct {
   Name string
   Age int
}

func (human Human) walk() {
   fmt.Println(human.Name, "walking.....")
}

执行上述代码,输出结果如下:

tony walking.....

Go 中没有面向对象的特性以及类的概念,但是可以通过结构体的内嵌和接口来实现比面向对象语言更高的扩展性和灵活性。

空接口

Go 中空接口可以直接当做任意类型来使用,空接口就相当于其他语言中的泛型,空接口既不定义任何方法,所以没有了方法的约束,任何类型的变量都可以实现空接口,可以表示任意数据类型。

空接口作为函数参数

func main() {

   paramWithEmptyInter(20)
   paramWithEmptyInter("你好 Go")
   slice_data := []int{1, 20, 300, 4}
   paramWithEmptyInter(slice_data)
}

func paramWithEmptyInter (zulu interface{}) {
   fmt.Printf("%v, %T\n", zulu, zulu)
}

执行上述代码,输出结果如下:

20, int
你好 Go, string
[1 20 300 4], []int

空接口作为函数参数,函数调用时入参没有任何限制,可以接收任意类型的输入作为入参。

定义 Map、Slice 时使用空接口类型

func main() {

   slice_data := []interface{}{"张三", 20, true, 1.0}
   fmt.Println(slice_data)

   map_data := make(map[string]interface{})

   map_data["name"] = "tony"
   map_data["age"] = 18
   map_data["balance"] = 0.0
   map_data["married"] = false

   fmt.Println(map_data)
}

执行上述代码,输出结果如下:

[张三 20 true 1]
map[age:18 balance:0 married:false name:tony]

Map 和 Slice 定义时存储的数据类型都是要提前定义好的,而且是不可以变化的,而是用空接口作为 Map 中 Value 的类型或者 Slice 中存储的元素类型,则可以实现 Slice 和 Map 中存储多种不同数据类型的数据

类型断言

一个接口的值是由一个具体类型和具体类型的值两部分组成,分别成为接口的动态乐行和动态值。

如果想要判断空接口中值的类型,则徐亚哦使用类型断言。

// x 表示类型为 interface{} 的变量
// T 表示断言 x 可能的类型
x.(T)
func main() {

   var x interface{}
   x = "Hello Go"
   v, ok := x.(string)

   if ok {
      fmt.Println("string 类型,值为:", v)
   } else {
      fmt.Println("不是 string 类型")
   }
}

执行上述代码,输出结果如下:

string 类型,值为: Hello Go

类型判断返回两个值,一个是变量的值,一个是布尔值表示是否是指定的类型。

值接收者和指针接收者

当方法作用于值类型接收者时,Go 会在代码运行时将接收者的值复制一份,在值类型接收者的方法中可以对接收者进行一些类操作,但是这些操作只针对复制的副本,并不会对原数据产生任何影响。

指针类型的接收者由一个结构体的指针组成,由于指针的特性,方法体内对接收者指针进行任何修改,都会作用在原数据上,就类似于面向对象语言中的 this 或者 self。

指针接收者在以下几种情况时会使用:

  • 需要修改接收者中的值
  • 接收者是拷贝代价较大的大对象
  • 保证一致性,如果有某个方法使用了指针接收者,那么其他方法也应该使用指针接收者。

本文正在参加技术专题18期-聊聊Go语言框架