GO语言基础:接口和结构体

11 阅读6分钟

一、结构体(Struct)

结构体是一种聚合数据类型,可以将零个或多个任意类型的变量组合在一起,形成一个新类型。每个变量称为结构体的字段

1. 结构体声明

使用 typestruct 关键字定义结构体:

type Person struct {
    Name   string
    Age    int
    Email  string
}
  • type Person struct:定义了一个名为 Person 的结构体类型。
  • 大括号内是字段列表,每个字段有名称和类型。

2. 结构体初始化

有多种方式创建结构体变量:

方式1:声明并初始化所有字段(按顺序)

var p1 Person = Person{"张三", 30, "zhangsan@example.com"}

缺点:必须按字段顺序提供所有值,且不能省略。

方式2:使用字段名(推荐)

p2 := Person{
    Name:  "李四",
    Age:   25,
    Email: "lisi@example.com",
}

字段顺序可以任意,未指定的字段会被赋予零值。

方式3:先声明后赋值

var p3 Person
p3.Name = "王五"
p3.Age = 28
p3.Email = "wangwu@example.com"

方式4:使用 new 关键字(返回指针)

p4 := new(Person)  // 等价于 &Person{}
p4.Name = "赵六"
p4.Age = 22

方式5:取地址符初始化

p5 := &Person{
    Name: "孙七",
    Age:  35,
}

3. 访问和修改字段

使用点号 . 访问或修改字段:

fmt.Println(p2.Name)  // 输出:李四
p2.Age = 26

4. 结构体嵌套(组合)

Go 没有继承,但可以通过嵌套结构体实现“组合”的效果。

type Address struct {
    City    string
    Street  string
}

type Employee struct {
    Name    string
    Age     int
    Address // 匿名字段(只有类型,没有字段名)
}

当使用匿名字段时,我们可以直接访问内嵌结构体的字段,就像它们是外层结构体的字段一样:

e := Employee{
    Name: "小张",
    Age:  28,
    Address: Address{
        City:   "北京",
        Street: "长安街",
    },
}
fmt.Println(e.City)    // 直接访问,输出:北京
fmt.Println(e.Street)  // 直接访问,输出:长安街

如果内嵌结构体和外层有同名字段,则需要通过内嵌类型名来访问(外层字段会“遮蔽”内层字段)。

5. 结构体方法

Go 可以为任何类型(包括结构体)定义方法,方法就是带接收者的函数。

// 值接收者
func (p Person) SayHello() {
    fmt.Printf("你好,我是 %s,今年 %d 岁。\n", p.Name, p.Age)
}

// 指针接收者(可以修改结构体内容)
func (p *Person) SetAge(age int) {
    p.Age = age
}

调用方法:

p := Person{Name: "小明", Age: 18}
p.SayHello()      // 输出:你好,我是小明,今年 18 岁。
p.SetAge(20)
fmt.Println(p.Age) // 20
  • 值接收者:方法内对接收者的修改不影响原变量。
  • 指针接收者:可以修改原变量,且避免复制大结构体时的开销。

6. 结构体标签(Tag)

结构体标签是附加在字段上的元信息,常用于序列化(如 JSON)、验证等场景。标签是字符串,通过反引号包裹。

type User struct {
    ID    int    `json:"id"`      // 转 JSON 时字段名为 id
    Name  string `json:"name"`
    Email string `json:"email,omitempty"` // 为空时忽略
}

使用 encoding/json 包进行序列化:

u := User{ID: 1, Name: "张三"}
data, _ := json.Marshal(u)
fmt.Println(string(data)) // {"id":1,"name":"张三"}

标签通过 reflect 包读取,框架或工具可以利用它们实现各种功能。


二、接口(Interface)

接口定义了一组方法签名,它只描述“能做什么”,而不关心具体的类型。在 Go 中,接口是隐式实现的:如果一个类型实现了接口中定义的所有方法,那么它就自动实现了该接口,无需显式声明。

1. 接口的定义

使用 typeinterface 关键字定义接口:

type Speaker interface {
    Speak() string
}

接口 Speaker 要求任何实现它的类型必须有一个 Speak() string 方法。

2. 接口的实现

假设我们有两个结构体:PersonDog,它们都实现了 Speak 方法:

type Person struct {
    Name string
}

func (p Person) Speak() string {
    return "你好,我是 " + p.Name
}

type Dog struct {
    Breed string
}

func (d Dog) Speak() string {
    return "汪汪!我是 " + d.Breed
}

现在,PersonDog 都隐式地实现了 Speaker 接口。

3. 使用接口实现多态

我们可以声明一个接口类型的变量,它可以保存任何实现了该接口的值:

func main() {
    var s Speaker

    s = Person{Name: "小明"}
    fmt.Println(s.Speak()) // 输出:你好,我是 小明

    s = Dog{Breed: "金毛"}
    fmt.Println(s.Speak()) // 输出:汪汪!我是 金毛
}

同一个接口变量,根据其底层具体类型的不同,表现出不同的行为,这就是多态

接口还可以作为函数参数,接受任何实现了该接口的类型:

func MakeSound(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    MakeSound(Person{Name: "小红"})
    MakeSound(Dog{Breed: "柯基"})
}

4. 空接口 interface{}any

空接口 interface{} 没有定义任何方法,因此所有类型都实现了空接口。它可以用来表示任意类型的值。从 Go 1.18 开始,anyinterface{} 的别名,含义相同。

var i interface{}
i = 42
i = "hello"
i = Person{Name: "测试"}

// 使用 any
var a any = 3.14

空接口经常用于需要处理未知类型的情况,但使用时需要配合类型断言来恢复具体类型。

5. 类型断言

类型断言用于提取接口值中的具体类型。语法:value, ok := 接口变量.(具体类型)ok 表示断言是否成功,避免 panic。

var s Speaker = Person{Name: "小明"}

// 类型断言
p, ok := s.(Person)
if ok {
    fmt.Println("s 是 Person 类型,姓名:", p.Name)
} else {
    fmt.Println("s 不是 Person 类型")
}

// 直接断言,不检查会 panic
// d := s.(Dog)  // panic

也可以使用 type switch 进行多种类型判断:

switch v := s.(type) {
case Person:
    fmt.Println("Person:", v.Name)
case Dog:
    fmt.Println("Dog:", v.Breed)
default:
    fmt.Println("未知类型")
}

6. 接口组合

接口可以通过嵌套其他接口来创建新接口,类似于结构体嵌套。例如:

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

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

// 组合接口
type ReadWriter interface {
    Reader
    Writer
}

任何类型只要同时实现了 ReaderWriter 接口的方法,就自动实现了 ReadWriter

7. 常用接口示例

Go 标准库中有许多有用的接口,例如 fmt.Stringer

type Stringer interface {
    String() string
}

任何实现了 String() string 方法的类型,在使用 fmt.Print 等函数时会自动调用该方法获取字符串表示。

func (p Person) String() string {
    return fmt.Sprintf("Person(Name=%s, Age=%d)", p.Name, p.Age)
}

func main() {
    p := Person{Name: "小明", Age: 20}
    fmt.Println(p) // 输出:Person(Name=小明, Age=20)
}

三、总结

  • 结构体:用于定义复杂数据类型,支持嵌套和方法,是数据组织的基石。
  • 接口:定义行为规范,支持隐式实现,是实现多态和解耦的关键工具。
  • 结构体和接口结合使用,可以构建出灵活、可扩展的程序。

接下来可以尝试自己编写一个小程序,例如:

  • 定义一个 Shape 接口,包含 Area() float64 方法
  • 实现 CircleRectangle 结构体,并计算面积
  • 使用接口变量存储不同形状,并输出面积

如果还有疑问,欢迎随时提问!你已经掌握了 Go 语言的核心内容,继续加油!