接口是一种聚合类型,结构体是和调用方的一种约定,有点抽象类的意思。:)
结构体
结构体定义
结构体是一种聚合类型,里面可以包含任意类型的值,这些值就是我们定义的结构体成员,也称为字段
在go语言中,要定义一个结构体,需要使用 type+struct 关键字组合。
// 定义一个代表【人】的结构体
type person struct {
name string // 名称
age uint // 年龄
}
结构体的成员字段并不是必需的,也可以一个字段都没有,这种结构体成为空结构体。
type s struct {}
结构体声明使用
// 使用var声明一个person变量,未初始化,里面的值为各自变量的零值
var p person
// 可以使用结构体字面量初始化的方式
p2 := person{"Mike", 10} // 第一个值为 name,第二个值为age,与结构体字段定义顺序有关
// 可以指定字段名初始化,不按定义顺序
p3 := person{age: 10, name: "Mike"}
字段结构体
type address struct {
province string
city string
}
// 结构体的字段可以是任意类型,也可以是自定义的结构体
type person struct {
name string
age uint
addr address
}
// 初始化
p := person{
name: "Mike",
age: 10,
addr: address{
province: "Guandong",
city: "Maoming",
},
}
fmt.Println(p.addr.province)
接口
接口的定义
接口是和调用方的一种约定,是一个高度抽象的类型,不需要和具体的实现细节绑定在一起。
接口要做的就是定义好约定,告诉调用方,自己可以做什么,但是不需要知道它的内部是如何实现的
我们通过 type + interface
关键字定义一个接口
type Stringer interface {
String() string
}
上述我们定义了一个接口 Stringer,这个接口有个方法 String() string
Stringer是Go SDK的一个接口,属于fmt包
接口的实现
接口的实现者必须是一个具体的类型
func (p person) String() string {
return fmt.Sprintf("name = %s, age = %d", p.name, p.age)
}
这里 person 实现了 Stringer接口的 String() 方法 我们接下来实现可以打印Stringer接口方法的函数:
func printString(s fmt.Stringer){
fmt.Println(s.String())
}
我们可以调用 printString(p)
来打印person的内容,因为 person实现了 fmt.Stringer 这个接口
同样,我们让 address 也实现Stringer接口
func (addr address) String() string {
return fmt.Sprintf("Addr province = %s, city = %s", addr.province, addr.city)
}
然后同样可以调用 printString 来输出: printString(p.addr)
这就是面向接口的好处,只要定义和调用双方满足约定,就可以使用,而不用管具体实现。接口的实现者也可以更好的升级重构,而不会有任何影响,因为接口约定没有变。
值接收者和指针接收者
调用printString(&p)
,可以发现以值类型接收者实现接口的时候,不管是类型本身,还是该类型的指针类型,都实现了改接口
但是,我们现在将接收者改为指针类型:
func (p *person) String() string {
return fmt.Sprintf("name: %s, age: %d", p.name, p.age)
}
然后调用 printString(p)
,将会提示编译不通过,因为person没有实现Stringer接口。
说明:以指针类型接收者实现接口的时候,只有对应的指针类型才被认为实现了该接口
接口实现规则
方法接收者 | 实现的接口类型 |
---|---|
(p person) | person 和 *person |
(p *person) | *person |
- 当值类型作为接收者时,person 类型和*person类型都实现了该接口。
- 当指针类型作为接收者时,只有*person类型实现了该接口。
工厂函数
工厂函数一般用于创建自定义的结构体,便于使用者调用
func NewPerson(name string) *person {
return &person{name: name}
}
p1 := NewPerson("Mike")
以 errors.New 这个 Go 语言自带的工厂函数为例,演示如何通过工厂函数创建一个接口,并隐藏其内部实现:
// 工厂函数,返回一个error接口,具体实现是*errorString
func New(text string) error {
return &errorString{text}
}
// 结构体,内部一个字段s,存储错误信息
type errorString struct {
s string
}
// 实现error接口
func (e *errorString) Error() string {
return e.s
}
继承和组合
在 Go 语言中没有继承的概念,所以结构、接口之间也没有父子关系,Go 语言提倡的是组合,利用组合达到代码复用的目的,这也更灵活。
以io标准包自带的接口为例:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter 是 Reader和Writer的组合
type ReadWriter interface {
Reader
Writer
}
ReadWriter 接口就是 Reader 和 Writer 的组合,组合后,ReadWriter 接口具有 Reader 和 Writer 中的所有方法,这样新接口 ReadWriter 就不用定义自己的方法了,组合 Reader 和 Writer 的就可以了。
接口可以组合,结构体也可以组合:
type address struct {
province string
city string
}
type person struct {
name string
age uint
address
}
// 初始化
p := person{
name: "mike",
age: 10,
address: address{
province: "Guangdong",
city: "Maoming",
},
}
// 像使用自己的字段一样使用address的字段
fmt.Println("my address is ", p.province, p.city)
类型组合后,外部类型不仅可以使用内部类型的字段,也可以使用内部类型的方法,就像使用自己的方法一样。如果外部类型定义了和内部类型同样的方法,那么外部类型的会覆盖内部类型,这就是方法的覆写。
type address struct {
province string
city string
}
type person struct {
name string
age uint
address
}
func (addr *address) detail() {
fmt.Printf("Address detail: province = %s, city = %s\n", addr.province, addr.city)
}
func (addr *address) who(){
fmt.Println("I am address.")
}
func (p *person) who() {
fmt.Println("I am person")
}
p := person{
name: "mike",
age: 10,
address: address{
province: "Guangdong",
city: "Maoming",
},
}
p.who() // 输出 I am person, 覆写了address.who 方法
p.address.who() // 可以调用 address.who,因为方法覆写不会影响到内部的方法实现
p.detail() // 输出 Address detail: province = Guangdong, city = Maoming, person没有实现该方法,因此直接调用address的detail方法
p.address.detail()// 输出 Address detail: province = Guangdong, city = Maoming
类型断言
类型断言用来判断一个接口的值是否实现改接口的的某个具体类型
<接口类型变量>.(断言类型)
如:
var s fmt.Stringer
s = p
// 断言接口的值 s 是否为一个 person
if _, ok := s.(person); ok {
fmt.Println("person yes")
}else {
fmt.Println("person no")
}
类型断言会返回两个值 value, ok
, 如果类型断言成功,value将会是一个断言对象的实例,如上述代码,断言成功后会返回一个person实例,ok=true,否则,ok=false
结构体是对现实世界的描述,接口是对某一类行为的规范和抽象。通过它们,我们可以实现代码的抽象和复用,同时可以面向接口编程,把具体实现细节隐藏起来,让写出来的代码更灵活,适应能力也更强。