Go语言入门指南Part 2 | 青训营

124 阅读12分钟

在Go语言入门指南Part1中,我们学习了Go语言中的变量、流程控制和容器。这一篇我们将学习Go语言剩下的入门内容。

1、函数

Go语言中拥有三种类型的函数:普通函数匿名函数/lambda方法

(1) 普通函数

普通函数的定义:

func 函数名 (形参数列表) (返回值列表) {
函数体
}

同时,Go语言支持多值返回,多返回值能方便地获得函数执行后的多个返回参数。一般的应用场景是除了调用函数接收信息外还额外接收一个err返回函数执行过程中可能发生的错误。 如果一组形参有相同的类型,那么我们只需要在最后一个写出其类型即可。如形参(x int,y int)可以写成(x,y int)

package main

import "fmt"

func add(x int, y int) int {
	return x + y
}
func add1(x, y int, s, t string) (int, string) { //多值返回,返回了int和string.
	return x + y, s + t
}
func main() {
	fmt.Println(add(1, 2))
	a, b := add1(2, 3, "ab", "c")
	fmt.Println(a, b)
}

输出结果为35 abc

带有变量名的返回值

Go语言支持我们对要返回的值在返回值列表进行命名,返回值和参数一样拥有类型和变量名,这样我们可以在函数体里对需要返回的变量进行计算。我们可以在return中不填写返回值列表(填写也是可以的)。 可以把前文的add函数改写成这样:

func add(x, y int) (ans int) {
	ans = x + y
	return
}

注意:两种返回方式只能二选一。

函数变量

函数也是一种类型。因此可以存储在变量中。

func add(x, y int) (ans int) {
	ans = x + y
	return
}
func main() {
	v := add
	fmt.Println(v(1, 2))
}

(2) 匿名函数

匿名函数的声明即在函数的声明里省去函数名。

我们可以在定义时直接调用匿名函数或者将匿名函数赋值给函数变量

//声明匿名函数时直接调用
	func(x int) {
		fmt.Println("This is", x)
	}(100)
	//将匿名函数赋值给函数变量
	print := func(x string) {
		fmt.Println("This is", x)
	}
	print("me")

或者我们可以通过定义一个接收匿名函数参数的函数,用作回调函数

package main

import (
	"fmt"
	"math"
)

func work(data []int, f func(a int) int) int {
	sum := 0
	for _, v := range data {
		sum += f(v)
	}
	return sum
}

func main() {
	a := []int{1, 2, 3, 4, 5}
	fmt.Println("数组的平方和为", work(a, func(x int) int {
		return x * x
	}))
	fmt.Println("数组的根号和为", work(a, func(x int) int {
		return int(math.Sqrt(float64(x)))
	}))
}

可以看到,在work()函数的参数里,我们传入了一个匿名/回调函数,使得每一个元素受到的处理由我们传入的匿名函数决定,在这个例子里,我们分别传入了平方和根号两种匿名函数,输出结果为数组的平方和为 55数组的根号和为 7

函数闭包

函数闭包是在函数内部引用了函数内部变量的函数,在Go语言中是通过匿名函数+引用环境实现的。

函数闭包可以对其作用域上的变量进行修改。而被闭包捕获的变量会随闭包的生命周期一直存在,如同拥有了记忆一样。

package main

import (
	"fmt"
)

// 函数闭包.在函数内部引用了函数内部变量的函数.使得函数外部可以访问函数内部的变量.
func incr(sum int) func(int) int {
	return func(i int) int {
		sum += i
		fmt.Printf("sum的地址是%p,值是%v\n", &sum, sum)
		return sum
	}
}
func main() {
	f1 := incr(1) //实例化sum为1的匿名函数
	f1(0)
	f1(1)
	f2 := incr(3) //实例化sum为2的匿名函数
	f2(0)
	f2(1)
}

在这个例子中,我们通过incr函数返回了一个匿名函数,它捕获了incr函数的自由变量sum,这个赋值给f1的匿名函数与sum组成了函数闭包,只要其实例f1没有消亡,sum都是引用传递。这意味着sumincr函数结束后并没有被销毁,而是发生了内存逃逸,逃逸到了堆上。 输出结果为sum的地址是0xc000016088,值是1sum的地址是0xc000016088,值是2sum的地址是0xc0000160c0,值是3sum的地址是0xc0000160c0,值是4。可以看到对于同一个匿名函数实例化的调用,被捕获的变量sum的地址是相同的,这更加说明被捕获变量在匿名函数的生命周期里都是引用传递的。

我们通过下面的例子加深这一点的理解:

package main

import (
	"fmt"
)

func main() {
	a := 1
	//匿名函数捕获了a,a是引用传递的
	defer func() {
		fmt.Println("引用传递", a)
	}()
	//直接把a的值赋值给参数列表.因此后面将a赋值为100并没有影响到
	defer func(a int) {
		fmt.Println("值传递", a)
	}(a)
	a = 100
}

输出结果为值传递 1引用传递 100。区别在于值传递时在defer定义时就已经把a=1赋值给了defer,运行时也是用的拷贝的a。而引用传递使用的a是当前环境中的a

defer

当有多个defer被注册时,将按照逆序执行。

package main

import (
	"fmt"
)

func main() {
	defer fmt.Printf("%v ", 1)
	defer fmt.Printf("%v ", 2)
	defer fmt.Printf("%v ", 3)
}

输出结果为3 2 1

2、结构体

(1) 结构体定义与初始化

结构体的定义方式如下:

type 结构体名 struct {

成员1 成员1类型

成员2 成员2类型

...

}

当多个字段拥有相同类型时,我们可以把它们写在同一行并只在最后一个声明(类似函数参数)。如x int y int可写为x y int。下面代码我们定义了一个点的结构体。

type point struct {
	x, y  float64
	value int
}

实例化结构体

在上一篇中我们已经学习了变量,所以当然可以用var关键字声明结构体并通过.来访问结构体的成员变量:

var p point
p.x,p.y,p.value = 1.0,1.0,2

在Go语言中,我们还可以用new关键字实例化结构体,会返回一个结构体的指针。与C++不同的是,Go语言中结构体指针访问成员变量时不需要使用->,可以继续使用.

func main() {
	var pt1 *point = new(point)
	pt1.x, pt1.y, pt1.value = 2.0, 3.0, 1
	pt2 := new(point)
	pt2.x, pt2.y, pt2.value = 1.0, -1.0, 2
	fmt.Println(pt1)
	fmt.Println(pt2)
}

除了new以外,我们还有另一种方法实例化结构体并得到结构体指针——取结构体的地址实例化。

func main() {
	pt2 := &point{} //取结构体的地址.
	pt2.x, pt2.y, pt2.value = 1.0, -1.0, 2
	fmt.Println(pt2)
}

初始化结构体

键值对初始化是在别的语言中比较常见的。而在Go语言中,键值对初始化的格式为:

变量名 := 结构体名{
成员1: 成员1的值,
成员2: 成员2的值,

}

特别注意初始化时每个成员后的,是不可省略的! 注意到这种方式初始化的成员是可选的,不在初始化列表中的成员实例化时会填入默认值。(如bool类型的默认值为Falseint类型为0)。

可以在键值对的基础上省去,也就是在上述初始化格式中省去成员x:。但是这样初始化必须注意: 1.必须初始化所有成员。2.初始值填充顺序与成员声明顺序一致。3.两种初始化方式不可混用

func main() {
	//键值对初始化,没有初始化value的值,所以默认为0.
	pt := point{
		x: 1.0,
		y: -1.0,
	}
	fmt.Println(pt)
        //列表初始化,必须初始化所有成员
	pt2 := point{
		1.0,
		-1.0,
		2,
	}
	fmt.Println(pt2)
}

匿名结构体

和匿名函数类似,匿名结构体没有名称。除此之外,我们可以在结构体中匿名地定义结构体字段,这样可以更好地实现我们的需求。

package main

import "fmt"

type person struct {
	name string
	//在结构体中匿名地定义结构体字段.
	address struct {
		city string
		room int
	}
}

//函数接收匿名结构体,打印结构体类型和值
func print(a *struct {
	x, y float64
}) {
	fmt.Printf("%T %v %v\n", a, a.x, a.y)
}
func main() {
	//匿名结构体
	a := &struct {
		x, y float64
	}{1.0,
		2.0,
	}
	print(a)
	p := person{name: "Lihua"}
	p.address.city = "Beijing"
	p.address.room = 10
	fmt.Println(p)
}

(2) 匿名字段

结构体可以包括一个或多个匿名字段,即这些字段只有变量类型而没有变量名。因此,每种类型只能有一个匿名字段。

package main

import "fmt"

type test struct {
	a int
	b string
	int \\匿名字段
	float64
}

func main() {
	a := test{
		10,
		"hello",
		20,
		1.5,
	}
	fmt.Println(a.int) \\直接访问匿名字段
	fmt.Println(a.float64)
}

内嵌结构体

结构体也是一种数据类型,因此其作为匿名字段使用时被称为内嵌结构体。内嵌结构体的一大特点就是不需要像传统结构体字段一样层层访问,而可以通过外部结构体的实例直接进行访问:

package main

import "fmt"

type color struct {
	R, G, B int
}
type ball struct {
	color //内嵌结构体
	size  int
}

func main() {
	a := ball{color{0, 255, 255}, 1}
	fmt.Println(a)
	fmt.Println(a.R) //内嵌结构体可以直接访问成员变量,即上下两种写法是等价的.
	fmt.Println(a.color.R)
}

3、接口

在Go语言中,接口定义了一个对象的行为。当一个类型实现了接口的所有方法,我们称该类型实现了这个接口。接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口,这称为非侵入式设计

(1) 接口声明和实现

type 接口类型 interface{

方法名( 参数列表 ) 返回值列表

方法名( 参数列表 ) 返回值列表

}

接口中声明的方法中参数变量名是可以忽略的。

type animal interface {
	meow()
	getID(string) int //忽略变量名
}

如果一个类型实现了接口的所有方法,那么就称该类型实现了此接口类型。需要注意的是,只要某个类型的方法不是接口方法的超集(即存在未被实现的方法),那么我们就无法将该类型的实例赋值给接口。

package main

import "fmt"

type animal interface {
	meow()
	getID(string) int //忽略变量名
}
type cat struct {
	name string
	id   int
}

//cat实现了animal接口的所有方法.
func (c cat) meow() {
	fmt.Printf("%T meow", c)
}
func (c cat) getID(string) int {
	return c.id
}
func main() {
	c := cat{"Alice", 1}
	var u animal = c //将接口赋值为c,即cat类型.
	u.meow()
}

类型实现接口的方法可以选择值传递或指针传递,因此,这两种写法是不同的:var u animal = cvar u animal = &c。需要指出的是,在Go语言中,若类型通过值传递实现了接口的某个方法,那么系统会自动生成指针传递的相应方法;但是反之则不会。可以看下面的例子:

//cat实现了animal接口的所有方法.
func (c cat) meow() { //值传递
	fmt.Printf("%T meow", c)
}
func (c *cat) getID(string) int { //指针传递
	return c.id
}
func main() {
	c := cat{"Alice", 1}
	var u animal = &c //值传递实现的方法自动生成指针传递,所以 *c类型已经实现了接口
	//var v animal = c 但是这样写就会报错.原因在于指针传递实现的方法不会自动生成值传递的
	u.meow()
}

(2) 类型断言

类型断言是作用在接口值上的操作,检查接口类型变量所持有的值。其格式如下:

val,ok := x.(T)

其中x表示作用的接口,T表示具体的类型。

var x interface{}
	x = "hello"
	val, ok := x.(string)
	fmt.Println(val, ok)
	val2, ok2 := x.(int)
	fmt.Println(val2, ok2)

因为空接口xstring的实例实现了,所以x的动态类型等于string,类型断言返回x的动态值,并返回结果为true,同理由于不为int,所以类型断言失败,返回false

因此输出结果为hello true0 false

类型断言和可以与switch结合使用,即x.(type)

func Output_type(x interface{}) {
	switch x.(type) {
	case int:
		fmt.Println("This is int")
	case string:
		fmt.Println("This is string")
	case float64:
		fmt.Println("This is float")
	default:
		fmt.Println("I don't know")
	}
}

func main() {
	Output_type("hello")
	Output_type(123)
	Output_type(1.5)
	Output_type(cat{"Alice", 1})
}

输出结果分别为This is stringThis is intThis is floatI don't know

当x是nil时,无论如何都会断言失败。

var x interface{}
	var u animal
	x = u
	val, ok := x.(animal)
	fmt.Println(val, ok)

输出结果为<nil> false

func main() {
	var x interface{}
	var u animal
	u = &cat{"aa", 1}
	x = u
	val, ok := x.(animal)
	fmt.Println(val, ok)
}

而这时的输出结果为&{aa 1} true

额外注意的是,当我们类型断言的T是一个接口时,那么就会看接口x对应的动态类型是否实现了接口T。我们可以看下面这个例子:

package main

import "fmt"

type animal interface { //动物接口
	eat()
}
type plant interface { //植物接口
	grow()
}

//具体类型.
type dog struct {
}
type tree struct {
}

func (d dog) eat() {
	fmt.Println("Dog is eating.")
}
func (c tree) grow() {
	fmt.Println("Tree is growing.")
}
func main() {
	collect := map[string]interface{}{
		"Dog": dog{}, "Tree": tree{},
	}
	for k, v := range collect {
		//判断是否为动物/植物,看空接口的实例是否实现了动物/植物接口
		val, okanimal := v.(animal)
		val2, okplant := v.(plant)
		fmt.Println("Name:", k, "Is animal:", okanimal, "Is plant:", okplant)
		//如果是调用相应接口的方法.
		if okanimal {
			val.eat()
		}
		if okplant {
			val2.grow()
		}
	}
}

(3) 排序

Go语言内置的排序接口sort.Interface由序列长度,两个元素比较的函数,两个元素交换的方式组成,即LenLessSwap。通过实现sort.Interface的这三个方法,那么我们就实现了这个接口,可以将参数传入sort.Sort()函数中,通过这样我们可以实现自定义排序。

package main

import (
	"fmt"
	"sort"
)

type person struct {
	age  int
	name string
}
type PersonList []person
//实现Sort.Interface接口的长度方法
func (p PersonList) Len() int {
	return len(p)
}
//比较方法
func (p PersonList) Less(i, j int) bool {
	if p[i].age != p[j].age {
		return p[i].age < p[j].age
	}
	return p[i].name < p[j].name
}
//交换方法
func (p PersonList) Swap(i, j int) {
	p[i], p[j] = p[j], p[i]
}
func main() {
	p := []person{person{11, "Cat"}, person{10, "Bob"}, person{10, "Alice"}}
	fmt.Println(p)
	sort.Sort(PersonList(p)) //PersonList实现了Sort.Interface,因此可以成功传参.
	fmt.Println(p)
}

排序后结果为[{10 Alice} {10 Bob} {11 Cat}]

(4) 接口嵌套

顾名思义,接口由其他接口嵌入,那么类型要实现这个接口必须实现其所有内嵌接口的方法。

type a interface {
	aa()
}
type b interface {
	bb()
}
type c interface {
	a
	b
}
type d struct {
}

func (D d) aa() {

}
func (D d) bb() {

}
func main() {
	var u c
	u = d{}
	val, ok := u.(d)
	fmt.Println(val, ok)
}

代码运行结果是类型断言正确