接口是一种聚合类型,结构体是和调用方的一种约定,有点抽象类的意思。:)

结构体

结构体定义

结构体是一种聚合类型,里面可以包含任意类型的值,这些值就是我们定义的结构体成员,也称为字段

在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

结构体是对现实世界的描述,接口是对某一类行为的规范和抽象。通过它们,我们可以实现代码的抽象和复用,同时可以面向接口编程,把具体实现细节隐藏起来,让写出来的代码更灵活,适应能力也更强。