Go学习笔记(三)
2019-08-05 本文已影响0人
dev_winner
Go函数
- Go语言中的函数可以
返回多个结果
。 -
函数类型的字面量
由关键字func
、由圆括号包裹参数声明列表
、空格
以及可以由圆括号包裹的结果声明列表
组成。每个参数声明由参数名称
、空格
和参数类型
组成。 - 参数声明列表中的
参数名称
是可以被统一省略的。结果声明列表中的结果名称
也是可以被统一省略的。并且,在只有一个无名称的结果声明
时还可以省略括号
,例如:
func(input1 string ,input2 string) string
加入type关键字
和一个标识符
作为名称,就变成了一个函数类型声明
:
type MyFunc func(input1 string ,input2 string) string
- 如果
结果声明是带名称
的,那么它就相当于一个已被声明但未被显式赋值的变量
。我们可以为它赋值且在return语句中省略掉需要返回的结果值
,如下所示:
func myFunc(part1 string, part2 string) (result string) {
result = part1 + part2
return
}
// 另一种常规写法:
func myFunc(part1 string, part2 string) string {
return part1 + part2
}
- 注意:
函数myFunc
是函数类型MyFunc
的一个实现
。实际上,只要一个函数的参数声明列表
和结果声明列表
中的数据类型的顺序
和名称
与某一个函数类型
完全一致,前者就是后者的一个实现
。 - 声明一个函数类型的变量,如:
var splice func(string, string) string // 等价于 var splice MyFun
// 然后把函数myFunc赋给它:
splice = myFunc
// 然后就可以调用该函数
splice("1", "2")
// 使用了一个匿名函数来初始化splice变量,
var splice = func(part1 string, part2 string) string {
return part1 + part2
}
// 省去splice变量
var result = func(part1 string, part2 string) string {
return part1 + part2
}("1", "2")
// 注意,result变量的类型不是函数类型,而与后面的匿名函数的结果类型是相同的。
- 匿名函数就是
不带名称
的函数值。匿名函数直接由函数类型字面量
和由花括号包裹的语句列表
组成。注意:这里的函数类型字面量中的参数名称
是不能被忽略
的。 - 函数类型的零值是
nil
。这意味着,一个未被显式赋值
的、函数类型的变量的值
必为nil
。
package main
import (
"fmt"
"strconv"
"sync/atomic"
)
// 员工ID生成器,定义别名
type EmployeeIdGenerator func(company string, department string, sn uint32) string
// 默认公司名称
var company = "Gophers"
// 序列号
var sn uint32
// 生成员工ID
func generateId(generator EmployeeIdGenerator, department string) (string, bool) {
// 若员工ID生成器不可用,则无法生成员工ID,应直接返回。
if generator == nil {
return "", false
}
// 使用代码包 sync/atomic 中提供的原子操作函数可以保证并发安全。
newSn := atomic.AddUint32(&sn, 1)
return generator(company, department, newSn), true
}
// 字符串类型和数值类型不可直接拼接,所以提供这样一个函数作为辅助转换。
func appendSn(firstPart string, sn uint32) string {
return firstPart + strconv.FormatUint(uint64(sn), 10)
}
func main() {
var generator EmployeeIdGenerator // 声明一个函数类型
generator = func(company string, department string, sn uint32) string { // 对应的一个实现
return appendSn(company+"-"+department+"-", sn)
}
fmt.Println(generateId(generator, "RD"))
}
// 输出结果:Gophers-RD-1 true
Go语言的结构体(Struct)
- 它可以封装
属性
和操作
。前者即是结构体类型中的字段
,而后者则是结构体类型所拥有的方法
。 -
结构体类型的字面量
由关键字type
、类型名称
、关键字struct
,以及由花括号包裹的若干字段声明组成。其中,每个字段声明独占一行
并由字段名称
(可选)和字段类型
组成。例如:
type Person struct {
Name string
Gender string
Age uint8
}
- 创建一个上述结构体类型的值:
Person{Name: "Robert", Gender: "Male", Age: 33}
-
结构体值的字面量
(或简称结构体字面量
)由其类型的名称和由花括号包裹的若干键值对
组成。注意,这里的键值对与字典字面量中的键值对
的写法相似,但不相同
。这里的键是其类型中的某个字段的名称
(注意,它不是字符串字面量
),而对应的值则是欲赋给该字段的那个值
。另外,若这里的键值对的顺序与其类型中的字段声明完全相同的话,就可以统一省略掉所有字段的名称,如:Person{"Robert", "Male", 33}
- 在编写某个
结构体类型的值字面量
时可以只对它的部分字段赋值
,甚至不对它的任何字段赋值
。这时,未被显式赋值的字段的值则为其类型的零值
。注意,在上述两种情况下,字段的名称
是不能被省略
的。 -
匿名结构体
:定义一个结构体值的字面量时不需要先拟好其类型。注意:需要写明其类型特征
(包含若干字段声明)和完成对应的值初始化。但一般建议使用正式的结构体定义。
p := struct {
Name string
Gender string
Age uint8
}{"Robert", "Male", 33}
func (person *Person) Grow() {
person.Age++
}
- 在关键字func和名称Grow之间的那个圆括号及其包含的内容就是
接收者声明
。其中的内容由两部分组成。第一部分是代表它依附的那个类型的值的标识符
。第二部分是它依附的那个类型的名称
。后者表明了依附关系
,而前者则使得在该方法中的代码可以使用到该类型的值
(也称为当前值
)。代表当前值的那个标识符可被称为接收者标识符
,或简称为接收者
。 - 注意:这种方式要写在结构体之外,因为接收者声明已经表示定义为某个结构体中的方法。
p := Person{"Robert", "Male", 33}
p.Grow()
直接在Person类型的变量p之上应用调用表达式来调用它的方法Grow。注意,Grow方法的接收者标识符person
指代的正是变量p的值
。这也是“当前值”
这个词的由来。在Grow方法的接收者声明中的那个类型是*Person
,而不是Person。实际上,前者是后者的指针类型
。与对象不同的是,结构体类型(以及任何类型)之间都不可能存在继承关系
。实际上,在Go语言中并没有继承
的概念。
- 结构体类型属于
值类型
。它的零值并不是nil
,而是其中字段的值均为相应类型的零值的值
。举个例子,结构体类型Person的零值
用字面量
来表示:Person{}
。
package main
import "fmt"
type Person struct {
Name string
Gender string
Age uint8
Address string
}
// 表示为结构体person的方法
func (person *Person) Move(newAddress string) string {
old := person.Address
person.Address = newAddress
return old
}
func main() {
p := Person{"Robert", "Male", 33, "Beijing"}
oldAddress := p.Move("San Francisco")
fmt.Printf("%s moved from %s to %s.\n", p.Name, oldAddress, p.Address)
}
Go语言的接口
- 在Go语言中,一个
接口类型
总是代表着某一种类型(即所有实现它的类型)的行为
。 - 一个接口类型的声明通常会包含关键字
type
、类型名称
、关键字interface
以及由花括号包裹的若干方法
声明。举个栗子:
type Animal interface {
// 若干方法的声明
Grow()
Move(string) string
}
- 注意,接口类型中的方法声明是
普通的方法声明的简化形式
。它们只包括方法名称
、参数声明列表
和结果声明列表
。其中的参数名称
和结果名称
都可以被省略
。出于文档化
的目的,最好写上:Move(new string) (old string)
。 - 如果一个数据类型
所拥有的方法集合
中包含了某一个接口类型中的所有方法声明的实现
,那么就可以说这个数据类型实现了那个接口类型
。实现一个接口中的方法是指:具有与该方法相同的声明
并且添加了实现部分
(由花括号包裹的若干条语句)。相同的方法声明
意味着完全一致的名称
、参数类型列表
和结果类型列表
。其中,参数类型列表
即为参数声明列表中除去参数名称的部分
。一致的参数类型列表
意味着其长度以及顺序的完全相同
。对于结果类型列表
也是如此。只要满足了“方法集合为其超集”
的条件,就建立了“实现”关系
。这是典型的无侵入式的接口实现方法
。 - 在Go语言中,判定
某种类型是否实现了某个接口
可以用类型断言
来实现。不能在一个非接口类型的值上应用类型断言
来判定它是否属于某一个接口类型
,必须先把前者
转换成空接口类型的值
。空接口类型
:不包含任何方法声明
的接口类型,用interface{}
表示,常简称为空接口
。正因为空接口的定义
,Go语言中的包含预定义的任何数据类型
都可以被看做是空接口的实现
。 - 直接使用
类型转换表达式
把一个*Person类型
转换成空接口类型的值
,例如:
p := Person{"Robert", "Male", 33, "Beijing"}
v := interface{}(&p)
注意:表达式&p
(&是取址操作符)的求值结果是一个*Person类型的值
,即p的指针
。在这之后,我们就可以在v上应用类型断言
了,即h, ok := v.(Animal)
。 类型断言表达式的求值结果可以有两个:第一个结果是被转换后的那个目标类型(这里是Animal)的值
,而第二个结果则是转换操作成功与否的标志(bool 类型),用来判定实现关系
的重要依据!
package main
import "fmt"
type Animal interface {
Grow()
Move(string) string
}
// 创建一个Cat结构体
type Cat struct {
Name string
Age uint8
Location string
}
// 定义结构体Cat的一个Grow方法
func (cat *Cat) Grow() {
cat.Age++
}
// 定义结构体Cat的一个Move方法
func (cat *Cat) Move(new string) string {
old := cat.Location
cat.Location = new
return old
}
// 结构体Cat实现了Animal接口
func main() {
myCat := Cat{"Little C", 2, "In the house"}
animal, ok := interface{}(&myCat).(Animal) // 转换成空接口类型值,应用断言表达式判断实现关系
fmt.Printf("%v, %v\n", ok, animal) // 输出结果:true, &{Little C 2 In the house}
}
- Grow和Move方法中只要有一个是
指针方法
,Person类型
就不可能是Animal接口的实现类型
。
Go语言指针
- 指针操作涉及到两个操作符——
&
和*
。当它们作为地址操作符
出现时,前者的作用是取址
,而后者的作用是取值
。通俗地讲,当地址操作符&
被应用到一个值上
时会取出指向该值的指针值
,而当地址操作符*
被应用到一个指针值上
时会取出该指针指向的那个值
。它们可以被视为相反的操作
。 除此之外,当*
出现在一个类型之前(如*Person
和*[3]string
)时就不能被看做是操作符
了,而应该被视为一个符号
。其要表达的含义是作为第二部分
的那个类型的指针类型
。把其中的第二部分所代表的类型称为基底类型
。例如,*[3]string
是数组类型[3]string的指针类型
,而[3]string
是*[3]string
的基底类型
。 - 只要一个方法的
接收者类型
是其所属类型的指针类型
而不是该类型本身
,那么我就可以称该方法为一个指针方法
。相对的,如果一个方法的接收者类型就是其所属的类型本身
,那么我们就称之为一个值方法
。例如:
func (person Person) Grow() {
person.Age++
}
// then operate below
p := Person{"Robert", "Male", 33, "Beijing"}
p.Grow()
fmt.Printf("%v\n", p)
// 输出结果可以发现,Age值不变,说明传的是一个值拷贝,而不是地址拷贝
原因是方法的接收者标识符
所代表的是该方法当前所属的那个值的一个副本
,而不是该值本身
。若传入的是一个指针类型值,这时的person代表的是p的值的指针的副本
。指针的副本
仍会指向p的值
。
package main
import "fmt"
type MyInt struct {
n int
}
func (myInt *MyInt) Increase() {
myInt.n++
}
func (myInt *MyInt) Decrease() {
myInt.n--
}
func main() {
mi := MyInt{}
mi.Increase()
mi.Increase()
mi.Decrease()
mi.Decrease()
mi.Increase()
fmt.Printf("%v\n", mi.n == 1)
}
- 一个
指针类型
拥有它本身
以及以它的基底类型为接收者类型的所有方法
,而它的基底类型
却只拥有以它本身为接收者类型的方法
。 - 在
基底类型的值
上仍然可以调用它的指针方法
。例如,若有一个Person类型的变量bp
,则调用表达式bp.Grow()是合法的。这是因为,如果Go语言发现调用的Grow方法是bp的指针方法
,那么它会把该调用表达式视为(&bp).Grow()
。
package main
import "fmt"
type Pet interface {
Name() string
Age() uint8
}
type Dog struct {
name string
age uint8
}
// 结构体Dog实现Animal接口中的方法
// 只要有一个是指针方法,dog类型就不可能是Pet接口的实现类型。
// Name()定义为值方法
func (dog Dog) Name() string {
return dog.name
}
// Age()定义为值方法
func (dog Dog) Age() uint8 {
return dog.age
}
func main() {
myDog := Dog{"Little D", 3}
_, ok1 := interface{}(&myDog).(Pet) // 将指针类型转化为空接口类型值,即使其实现的都是值方法,依然实现了Pet接口。
_, ok2 := interface{}(myDog).(Pet) // 其实现方法都为值方法,否则为false
fmt.Printf("%v, %v\n", ok1, ok2) // true, true
}