【Go结构体和接口】

结构体

结构体定义

​ 结构体是一种聚合类型,里面可以包含任意类型的值,这些值就是我们定义的结构体的成员,也称为字段。在Go语言中,要定义一个结构体,需要使用type + struct 关键字组合

// 结构体定义表达式
type structName  struct{
    fieldName1 typeName
    ...
    ...
}

type Person struct{
    name string
    age uint
}

​ 定义结构体时,字段的声明方法和变量是一样的,都是变量名称在前,类型在后,只不过在结构体中,变量名称为字段名或成员名。

​ 结构体的成员字段不是必须的,也可以一个字段都没有,这种结构体称为空结构体

  • type 和 struct 是Go语言的关键字,二者组合就代表要定义一个新的结构体类型;
  • structName 是结构体类型的名字;
  • fieldName 是结构体的字段名,而typeName 是对应字段的类型;
  • 字段可以是零个、一个或者多个

结构体也是一种类型,所以自定义的结构体,会称为某结构体或某类型,二者是一个意思。

结构体声明使用

​ 结构体类型和普通的字符串、整型一样,可以使用同样的方式声明和初始哈。如果声明了一个结构体类型,没有初始化,那么会默认使用结构体里字段的零值。

​ 在声明一个结构体变量的时候,可以通过结构体字面量的方式初始化。

var  p  person 

// 采用简短声明法,同事采用字面量初始化方式,把结构体变量p的name初始化为zhangsan,age初始化为18,以逗号分隔
p := person{"zhangsan", 18}

// 指出字段名称
p := person{age:18, name:"zhangsan"}

// 只初始化个别字段,其余字段使用默认空值
p := person{age:18}

​ 在Go语言中,访问一个结构体字段和调用一个类型的方法一样,都是通过点操作符"."

​ 采用字面量初始化结构体时,初始化值的顺序很重要,必须和字段定义的顺序一致,才能编译通过。

​ 当然,也可以不按照顺序初始化,只不过要指出字段名称。因为采用了明确的 field:value 方式进行指定,这样Go语言编译器会清晰的知道要初始化哪个字段值。 这种方式和map类型的初始化很像,都是采用冒号分割。Go语言尽可能的重用操作,不发明新的表达式,便于我们记忆使用。

字段结构体

​ 结构体的字段可以使任意类型,也包括自定义的结构体类型。

type person struct {
    name string
    age uint
    addr address
}

type address struct {
    province string
    city string
}

​ 上述示例中,我们定义了两个结构体:person代表人,address代表地址。在结构体person中,有一个address类型的字段addr,这就是自定义结构体。

​ 这种方式,用代码描述现实中 实体会更匹配,复用程度也更高。对于嵌套结构体字段的结构体, 其初始化和正常结构体大同小异,只需要根据字段对应的类型初始化即可。

p :=person {
    age : 18,
    name : "zhangsan",
    addr : address {
        province : "beijing",
        city : "beijing",
    },
}

// 依然是通过点操作符访问结构体里面的字段值
p.addr.city

接口

接口的定义

​ 接口是和调用法的一种约定,它是高度抽象的类型,不用和具体的实现细节绑定在一起。接口要做的就是定义好约定,告诉调用方自己可以做什么,但不用知道内部实现。

​ 接口的定义和结构体稍微有些区别,它们都是以 type 关键字开始,但是接口的关键字是interface,表示自定义的类型是一个接口。

// Stringer 是Go SDK 的一个接口,属于fmt包
type Stringer interface {
    String() string
}
// 针对stringer接口来说,告诉调用者可以通过String()方法获得一个字符串,这就是接口的约定。至于字符串如何获得,长什么样,接口不关心,调用者也不用关心,这些都是由接口调用者实现。

接口的实现

​ 接口的实现者必须是一个具体的类型

func (p person) String() string {
    return fmt.Sprintf("the name is %s, age is %d", p.name, p.age)
}

func (addr address) String() string {
    return fmt.Sprintf("the addr is %s%s", addr.province, addr.city)
}

func printString(s fmt.Stringer) {
    fmt.Println(s.String())
}

​ 结构体类型person定义了一个方法,这个方法和接口里**方法的签名(名称、参数和返回值)**一行, 这样结构体person就实现了 Stringer 接口。 注意:如果一个接口里有多个方法,那么要实现接口的每个方法才算实现了这个接口。

​ 如上所示,被定义的printString函数, 它接收一个Stringer接口类型的参数,然后打印出Stringer接口的String方法返回的字符串。printString函数的优势在于是面向接口编程的,只要一个类型实现了Stringer接口,都可以打印出对应的字符串,不用管具体实现。

​ 同样的,结构体address也实现了Stringer接口,所以printString函数不用做任何改变,可以直接被使用。

​ 面向接口编程的好处,只要定义和调用双方满足约定,就可以使用,不用管具体实现。接口的实现着也可以更好的升级重构,而不会有任何影响,因为接口约定没有变。

值接收者和指针接收者

​ 要实现一个接口,必须要实现这个接口提供的所有方法。定义一个方法,有值类型的接收者和指针类型的接收者。二者都可以调用方法,因为Go语言编译器自动做了转换,所以值类型接收者和指针类型接收者是等价的。但是在接口的实现中,值类型接收者和指针类型接收者不一样。

printString(&p)
// 把变量p的指针作为实参传给printString 函数也是可以的,编译运行都正常。证明: 以值类型接收者实现接口的时候,不管是类型本身,还是该类型的指针类型,都实现了该接口

​ 但是,如果已指针类型作为接收者时,编译器会报错

func (p *person) String() string {
    return fmt.Sprintf("the name is %s, age is %d", p.name, p.age)
}

// 报错信息
./main.go:17:13: cannot use p (type person) as type fmt.Stringer in argument to printString:
	person does not implement fmt.Stringer (String method has pointer receiver)

​ 意思是类型person没有实现Stringer接口。证明已指针类型接收者是吸纳接口的时候,只有对应的指针类型才被认为实现了该接口

方法接收者实现接口的类型
(p person)person 和 *person
(p *person)*person
  • 当值类型作为接收者时,person类型和*person类型都实现了该接口
  • 当指针类型作为接收者时,只有*person类型实现了该接口

工厂函数

​ 工厂函数一般用于创建自定义的结构体,便于使用者调用。

func NewPerson(name string) *person {
    return &person{name : name}
}

​ 通过工厂函数创建自定义结构体的方式,可以让调用者不用太关注结构体内部的字段,只需要给工厂函数传参就可以了。

​ 工厂函数也可以用来创建一个接口,它的好处就是可以隐藏内部具体类型的实现,让调用者只关注接口的使用即可。

// 工厂函数,返回一个error接口,其具体是吸纳是*errorString
func New(text string) error {
    return &errorString(text)
}

// 结构体,内部一个字段s,存储错误信息
func errorString struct {
    s sting
}

// 用于实现error接口
func (e *errorString) Error() string {
    return e.s
}

​ errorString 是一个结构体类型,它实现了Error接口,所以可以通过New工厂函数,创建一个*errorString类型,通过接口error返回。这就是面向接口的编程,假设重构代码,即使是换一个结构体实现error接口,对调用者也没有影响,因为接口没变。

继承和组合

​ 在Go语言中没有继承的概念,所以结构、接口之间也没有父子关系,Go语言提倡的是组合,利用自核达到代码复用的目的,这也更灵活。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// ReadWriter 是Read 和 Write 的组合
type ReadWriter interface {
    Reader 
    Writer
}


type person struct {
    name string
    age uint
    address
}

​ ReadWriter接口就是Read和Writer的组合,组合后,该接口具有两者中所有的方法,不用再定义新方法了。

不止接口可以组合,结构体也可以组合。上述示例中,直接把结构体类型放进去,就是组合,不需要字段名。组合后,被组合的address称为内部类型,person称为外部类型。当然,修改了person结构体类型后,声明和使用也要修改,如下:

P := person {
    name : "zhangsan",
    age : 18,
    address : address {
        province : "beijing",
        city : "beijing",
    },
}


p.province 

​ 类型组合后,外部类型不仅可以使用内部类型的字段,也可以使用内部类型的方法,就像使用自己的方法一样。如果外部类型定义了和内部类型同样的方法,那么外部类型会覆盖内部类型,就是方法覆写。方法覆写不会影响内部类型的的方法实现。

类型断言

​ 有了接口和实现接口的类型,就会有类型断言。类型断言用来判断一个接口的值是否是实现该接口的某个具体类型。

​ Go语言提供类型断言的多值返回。类型断言返回的第二个值,就是断言是否成功的标志,如果是true则成功,否则失败。

func (p *person) String() string {
    return fmt.Sprintf("the name is %s, age is %d", p.name, p.age)
}

func (addr address) String() string {
    return fmt.Sprintf("the addr is %s%s", addr.province, addr.city)
}

var s fmt.Stringer
s = p1
p2 := s.(*person)
fmt.Println(p2)

// p2已经是*person类型了,在类型断言的时候,同时完成了类型转换。

​ 接口变量s称为接口fmt.Springer的值,赋值为p1。然后使用类型断言表达式s.(*person),尝试返回一个p2。如果接口的值是一个person,那么类型断言正确,可以正常返回p2。如果不是,则在运行是会抛出异常,程序终止运行。

// 下面这段代码在编译时不会报错,因为接口address也实现了接口Stringer,但是在运行的时候,会抛出异常
// panic: interface conversion: fmt.Stringer is *main.person, not main.address

a := s.(address)
fmt.Println(a)


// 可以利用类型断言的第二个值,优化一下
if a,ok := s.(address); ok {
    fmt.Println(a)
} eles {
    fmt.Println("s不是一个address")
}

小结

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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值