前言
这是我学习Go语法的笔记。由于有C和Python的基础,上手Go很快。笔记很粗糙,好在自己够用。
此篇包括了Go相关的:匿名组合,方法,接口等。涉及关键字interface
。
匿名组合
Go语言没有封装、继承、多态的概念,但可以通过别的方式实现这些特性:
- 封装:通过方法实现。
- 继承:通过匿名字段实现。
- 多态:通过接口实现。
匿名字段
理解Go语言中匿名字段不妨类比其他的语言的“类和继承”的概念,这里我用Python举例。
当我们需要避免一些重复工作时——比方现在我们需要两种人,一种学生,一种上班族,代码可以如下:
# 学生
class Student(object):
name = ""
age = 0
gender = 1 # 1表示男生
school = ""
# 上班族
class OfficeWorker(object):
name = ""
age = 0
gender = 1
company = ""
很好,代码写完了……欸卧槽?!怎么重复了name、age、gender?
事实上,但凡一个人,都有应该有名字年纪和性别。所以“学生”与“上班族”之间存在相同的成员变量不足为奇,而程序员为了“偷懒”,一拍脑袋想出了继承这一概念,于是:
class Person(object):
name = ""
age = 0
gender = 1
class Student(Person):
school = ""
class OfficeWorker(Person):
company = ""
我们称Student和OfficeWorker是Person的派生类,它们继承自Person。
现在Go语言的程序员也想偷懒,因此有了匿名字段,上述内容用Go实现如下:
type Person struct {
name string
age int
gender byte
}
type Student struct {
Person // 结构体匿名字段
school string
}
type OfficeWorker struct {
Person
company string
}
此时结构体Person就作为结构体Student的匿名字段。很显然,由于Person充当的是一个数据类型(声明一个Person类型的结构体语法:var p Person
),所以对于结构体Student来说,数据类型为Person的变量名并不存在,那么,就“匿名”了!且Student包含Person中的所有字段。
结构体匿名字段
也就是说,这个匿名变量,其类型为结构体。因此,前面说的Person,在Student中就起到了辅助匿名变量的任务。
初始化赋值:
对于初始化一个Student类型的变量,其方法可以如下几种:
var stu = Student{Person{"ty", 22, 'm'}, "XHUnivercity" } // 顺序初始化
var stu = Student{Person{age:22}, "XHUnivercity"} // 对于Person来说是指定初始化,对整个Student来说,是顺序初始化
var stu = Student{Person:Person{age:22}} // 整个Student为指定初始化
stu := Student{Person{"ty", 22, 'm'}, "XHUnivercity" } // 简短声明
当匿名字段的数据类型为指针时,需要加上取地址符(&
):
type Student struct {
*Person // 结构体匿名字段
school string
}
var stu = Student{&Person{"ty", 22, 'm'}, "XHUnivercity" } // &Person
或者用new()
提前分配空间,再用点操作访问成员变量:
var stu Student
stu.Person = new(Person)
stu.name = ...
...
非初始化赋值:
非初始化赋值有三种方法:
type Person struct {
name string
age int
gender byte
}
type Student struct {
Person
school string
}
func main() {
var stu1, stu2, stu3 Student
// 第一种方法
stu1.name = "ty"
stu1.age = 22
stu1.gender = 'm'
stu1.school = "XHUnivercity"
fmt.Printf("%+v\n", stu1)
// 第二种方法
stu2.Person.name = "ty"
stu2.Person.age = 22
stu2.Person.gender = 'm'
stu2.school = "XHUnivercity"
fmt.Printf("%+v\n", stu2)
// 第三种方法
stu3.Person = Person{"ty", 22, 'm'}
stu3.school = "XHUnivercity"
fmt.Printf("%+v\n", stu3)
}
// 输出:
{Person:{name:ty age:22 gender:109} school:XHUnivercity}
{Person:{name:ty age:22 gender:109} school:XHUnivercity}
{Person:{name:ty age:22 gender:109} school:XHUnivercity}
同名字段
在两个具有“父子”关系的类中,如果出现相同名字的成员变量,子类的实例化对象会优先访问子类中成员变量。这样的结论也适用于Go语言。
首先,当一个结构体中的使用了另一个结构体,且两个结构体中存在相同名称的成员变量,我们称这个变量为同名字段。就像下边的salary一样:
type Boyfriend struct{
salary int
}
type Girlfrien struct{
Boyfriend
salary int
}
func main() {
var girl = Girlfrien{Boyfriend{30000}, 30000}
fmt.Printf("%+v\n", girl)
var bagPrice = 4000
girl.salary -= bagPrice // 女生要买包包了(直接点操作)
fmt.Printf("%+v\n", girl)
}
// 输出:
{Boyfriend:{salary:30000} salary:30000}
{Boyfriend:{salary:30000} salary:26000}
这是可以理解的:尽管男友每个月上交工资,但女生仍要清楚,自己仅仅拥有随意花自己钱的权利。所以直接点操作访问的是自己的salary,如果需要用男友的钱买包包,我建议经过男友的同意比较好:
girl.Boyfriend.salary -= bagPrice // 使用 Boyfriend 中的salary
非结构体匿名字段
并非匿名字段的数据类型只能是结构体,基础类型、自定义类型也可以:
type mystr string
type Person struct{
name string
}
type Student struct {
Person // 结构体匿名字段
int // 基础类型匿名字段
mystr // 自定义类型匿名字段
}
func main() {
stu := Student{Person{"ty"}, 22, "XHUnivercity"}
fmt.Printf("%+v\n", stu)
// 也可以
fmt.Println(stu.Person, stu.int, stu.mystr) // 只有匿名字段可以 点 + 数据类型
}
// 输出:
{Person:{name:ty} int:22 mystr:XHUnivercity}
{ty} 22 XHUnivercity
需要注意两点:
- 只有匿名字段可以
点 + 数据类型
方式的访问; - 对匿名字段的要求:一个结构体中不应该存在相同类型的匿名字段。
方法
什么是方法
大部分语言中方法和函数是相同的概念,Go语言却不是,快速入门Go2 中提到的是函数,格式如:func funcName(paramsList) (returnList) {}
而方法格式为:func (receiver ReceiverType) funcName(paramsList) (returnList) {}
Go语言中,可以给任意自定义类型添加相应的方法。比方说:
type zty string
func (z zty) greet(words string) {
fmt.Printf("%s\n", words)
}
func main() {
var z zty
z.greet("hello world") // 对于类型为zty的变量来说,它们都拥有了greet()这个方法
}
// 输出:
hello world
如果方法配合结构体一起使用,有没有很像其他语言中的类呢!
关于方法的使用需要注意以下几点:
- 参数receiver可以任意命名,如果方法中未曾使用,可省略参数名。
- 参数receiver类型可以是T或*T。基类型T不能是接口或指针。
- 不支持重载。(只要接收者类型不一样,此时认为T与*T是同一种类型,这个方法就算同名也是不同的方法,不会出现重复定义函数的错误。)
值语义和引用语义
值语义表示方法的接收者为普通变量,非指针。即,通过拷贝方式传入:
type student struct{
name string
age int
id int64
}
func (s student) setInfoValueByValue(name string, age int, id int64) {
s.name = name
s.age = age
s.id = id
}
func main() {
var s student
s.setInfoValueByValue("ty", 22, 201431060011)
fmt.Printf("name = %s, age = %d, id = %d", s.name, s.age, s.id)
}
// 输出:
name = , age = 0, id = 0
引用语义表示接收者为指针类型的变量,利用引用传递:
type student struct{
name string
age int
id int64
}
func (s *student) setInfoValueByPointer(name string, age int, id int64) {
s.name = name
s.age = age
s.id = id
}
func main() {
var s student
p := &s
p.setInfoValueByPointer("ty", 22, 201431060011)
fmt.Printf("name = %s, age = %d, id = %d", s.name, s.age, s.id)
}
// 输出:
name = ty, age = 22, id = 201431060011
所以二者最大的区别在于:能不能通过方法来改变自身的值。
方法集
即:可以被该类型的值调用的所有方法的集合。
由于Go语言中的指针与普通变量使用起来很灵活,所以可以通过转换调用彼此的方法:
type me string
func (m me) PrintAlpha() {
fmt.Println("a, b, c")
}
func (m *me) PrintDigit() {
fmt.Println("1, 2, 3")
}
func main() {
var m me
m.PrintAlpha()
(&m).PrintDigit()
fmt.Println("-------- 分割线 -------- ")
p := new(me)
(*p).PrintAlpha()
p.PrintDigit()
}
// 输出:
a, b, c
1, 2, 3
-------- 分割线 --------
a, b, c
1, 2, 3
事实上编译器可以自动识别并做类型转换,所以上述代码可以修改两句:
(&m).PrintDigit()
// ...
(*p).PrintAlpha()
// 改为
m.PrintDigit()
// ...
p.PrintAlpha()
方法的继承
当结构体B中包含了结构体A,那么结构体B就拥有了结构体A中的所有方法。
方法的重写
当结构体B中包含了结构体A,为结构体B绑定的方法恰好与结构体A中的方法同名,此时认为重写了这个方法。在调用过程中,结构体B类型的变量会优先调用结构体B上绑定的方法,如果没有,才会去结构体A中寻找。
方法表达式
即:记录了指定方法的地址的指针。使用方法表达式时,第一个参数一定是这个方法的接收者类型的变量。
func (me) greet(words string) {
fmt.Println(words)
}
func main() {
var m me
p := (me).greet // p为方法表达式
p(m, "hello world") // 使用方法表达式,第一个参数一定是这个方法的接收者类型的变量
}
// 输出:
hello world
接口
Go语言中,接口是一个自定义类型,作为描述一系列方法的集合。关于接口需要注意以下几点:
- 接口命名习惯以er结尾。
- 接口只有方法声明,没有实现,没有数据字段。
- 接口可以匿名嵌入其他接口,或嵌入到结构中。
接口的定义
接口的定义类似结构体定义:
type Humaner interface {
// ...
}
接口的使用
利用接口,可以很好的实现多态:
type Humaner interface {
sayWords() // 方法声明
}
type (
personOne string
personTwo string
)
func (personOne) sayWords() {
fmt.Printf("我是 personOne,很高兴认识大家\n")
}
func (personTwo) sayWords() {
fmt.Printf("在下 personTwo,庆幸相遇\n")
}
// 注意参数类型
func sayWords(i Humaner) {
i.sayWords()
}
func main() {
var (
one personOne
two personTwo
)
// 同一个函数,实现不同的功能
sayWords(one)
sayWords(two)
}
// 输出:
我是 personOne,很高兴认识大家
在下 personTwo,庆幸相遇
接口的继承与结构体相似,接口继承又被称为接口嵌用。
接口的转换
Go语言中,作为超集的接口可以转化为子集接口,反之却不行。我们对超、子集接口有如下定义:
type Oner interface { // 子集
sayWords()
}
type Twoer interface { // 超集,比子集 Oner 多了一个sing()方法
Oner // 匿名嵌入
sing()
}
所以此时可以超集转子集:
var one Oner
one = me("one")
var two Twoer
two = me("two")
one = two // 正确
one.sayWords()
绝不可以子转超:
two = one // 错误
two.sayWords()
空接口
你可以将空接口这个概念认为是C语言中的void *
,因为空接口可以存储任意类型的值(interface{}
)。
func main() {
var i interface{} = 1
fmt.Printf("类型:%T, 值:%v\n", i, i)
i = "abc"
fmt.Printf("类型:%T, 值:%v\n", i, i)
i = 5.12
fmt.Printf("类型:%T, 值:%v\n", i, i)
}
// 输出:
类型:int, 值:1
类型:string, 值:abc
类型:float64, 值:5.12
因为空接口可以存储任意类型的变量,而有时候我们又需要知道变量的类型,此时就有了类型断言的概念。一般而言有两种方法,第一种利用if ... else if...
,第二种switch
。
// if ... else if ...
func main() {
var i interface{} = 1
if value, ok := i.(string); ok {
fmt.Printf("i 的类型是string, i 的值是 %s", value)
} else if value, ok := i.(int); ok {
fmt.Printf("i 的类型是int, i 的值是 %d", value)
}
}
func main() {
var i interface{} = 1
switch value := i.(type) { // .(type) 这样的操作只能在switch中使用
case int:
fmt.Printf("i 的类型是int, i 的值是 %d", value)
case string:
fmt.Printf("i 的类型是string, i 的值是 %s", value)
}
}
还不快抢沙发