Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go语言结构体和接口

Go语言结构体(Struct)和接口(Interface)详解

作者:数据知道

本文主要介绍了Go语言结构体(Struct)和接口(Interface),详解其定义、匿名字段、值/指针接收器等特性,探讨接口的隐式实现、类型断言及组合应用,下面就一起来了解一下

Go 语言中两个至关重要的概念:结构体(Struct)接口(Interface)。它们是 Go 语言实现面向对象编程思想的核心,理解它们是编写复杂、可扩展 Go 应用的关键。
本文将分为以下几个部分:

  1. 结构体详解
    • 什么是结构体?
    • 结构体的定义与实例化。
    • 结构体的匿名字段与嵌套(组合)。
    • 结构体与方法(值接收器 vs 指针接收器)。
  2. 接口详解
    • 什么是接口?
    • 接口的定义与隐式实现。
    • 空接口 interface{} 与类型断言。
    • 接口的组合。
    • 接口的最佳实践。
  3. 结构体与接口的协同工作:通过一个综合案例,展示如何利用结构体和接口设计出灵活、可扩展的系统。

一、结构体详解

1.1 什么是结构体?

结构体是一种聚合类型,里面可以包含任意类型的值,这些值就是我们定义的结构体的成员,也称为字段。在 Go 语言中,要自定义一个结构体,需要使用 type+struct 关键字组合。它允许你将不同类型的数据项(字段)组合成一个单一的实体,类似于其他语言中的“类”或“对象”。结构体是值类型。

1.2 结构体的定义与实例化

定义:使用 typestruct 关键字。type 和 struct 是 Go 语言的关键字,二者组合就代表要定义一个新的结构体类型。

// 定义一个名为 Person 的结构体
type Person struct {
    FirstName string
    LastName  string
    Age       int
    IsActive  bool
}

实例化:创建结构体变量的多种方式。

package main
import "fmt"
type Person struct {
    FirstName string
    LastName  string
    Age       int
}
func main() {
    // 方式1:声明一个变量,默认为零值
    var p1 Person
    fmt.Printf("p1: %+v\n", p1) // 输出: p1: {FirstName: LastName: Age:0}
    // 方式2:使用字面量创建(推荐)
    p2 := Person{
        FirstName: "Alice",
        LastName:  "Smith",
        Age:       30,
    }
    fmt.Printf("p2: %+v\n", p2) // 输出: p2: {FirstName:Alice LastName:Smith Age:30}
    // 方式3:使用字面量创建(按顺序,不推荐,易错)
    p3 := Person{"Bob", "Johnson", 25}
    fmt.Printf("p3: %+v\n", p3) // 输出: p3: {FirstName:Bob LastName:Johnson Age:25}
    // 方式4:创建一个指向结构体的指针
    p4 := &Person{
        FirstName: "Charlie",
        LastName:  "Brown",
        Age:       40,
    }
    fmt.Printf("p4: %+v, Type: %T\n", p4, p4) // 输出: p4: &{FirstName:Charlie LastName:Brown Age:40}, Type: *main.Person
    // Go 会自动解引用,可以直接通过指针访问字段
    fmt.Println("p4's first name:", p4.FirstName) // 输出: p4's first name: Charlie
}

1.3 结构体的匿名字段与嵌套(组合)

Go 语言没有继承,但它通过结构体嵌套实现了组合,这是一种更灵活的代码复用方式。

package main
import "fmt"
// 定义一个基础结构体
type Address struct {
    Street, City, Country string
}
// 定义一个 Person 结构体,嵌套了 Address
// Address 是一个匿名字段,因为它没有名字
type Person struct {
    Name string
    Age  int
    Address // 匿名字段
}
func main() {
    p := Person{
        Name: "David",
        Age:  35,
        Address: Address{
            Street:  "123 Go Lane",
            City:    "Golang City",
            Country: "GoLand",
        },
    }
    // 访问嵌套结构体的字段
    fmt.Println("Name:", p.Name)
    // 可以直接访问嵌套结构体的字段,这被称为“提升”(Promotion)
    fmt.Println("City:", p.City) // 等同于 p.Address.City
    fmt.Println("Full Address:", p.Address.Street, p.Address.City, p.Address.Country)
}

组合的优势Person“拥有”一个 Address,而不是“是一个”Address。这种关系更加灵活,避免了继承带来的复杂性和紧耦合。

1.4 结构体与方法

方法是一种带有特殊接收器参数的函数。接收器可以是结构体类型或其指针类型。

package main
import "fmt"
type Rectangle struct {
    Width, Height float64
}
// 值接收器:操作的是结构体的副本,不会修改原始结构体
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}
// 指针接收器:操作的是结构体本身,会修改原始结构体
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}
func main() {
    rect := Rectangle{Width: 10, Height: 5}
    // 调用值接收器方法
    area := rect.Area()
    fmt.Printf("Original Area: %.2f\n", area) // 输出: Original Area: 50.00
    // 调用指针接收器方法
    // Go 会自动将 rect 转换为 &rect,这是语法糖
    rect.Scale(2)
    fmt.Printf("Scaled Rectangle: %+v\n", rect) // 输出: Scaled Rectangle: {Width:20 Height:10}
    fmt.Printf("New Area: %.2f\n", rect.Area()) // 输出: New Area: 200.00
}

值接收器 vs 指针接收器

二、接口详解

2.1 什么是接口?

接口是和调用方的一种约定,它是一个高度抽象的类型,不用和具体的实现细节绑定在一起。接口要做的是定义好约定,告诉调用方自己可以做什么,但不用知道它的内部实现,这和我们见到的具体的类型如 int、map、slice 等不一样。

接口的定义和结构体稍微有些差别,虽然都以 type 关键字开始,但接口的关键字是 interface,表示自定义的类型是一个接口。也就是说 Stringer 是一个接口,它有一个方法 String() string,整体如下面的代码所示:

// 提示:Stringer 是 Go SDK 的一个接口,属于 fmt 包。
type Stringer interface {
    String() string
}

针对 Stringer 接口来说,它会告诉调用者可以通过它的 String() 方法获取一个字符串,这就是接口的约定。至于这个字符串怎么获得的,长什么样,接口不关心,调用者也不用关心,因为这些是由接口实现者来做的。

接口是一种抽象类型,它定义了一组方法签名(方法名、参数、返回值),但没有实现。接口规定了“做什么”,但不规定“怎么做”。任何类型只要实现了接口中定义的所有方法,就被称为实现了该接口,无需像 Java 或 C# 那样显式声明。

这种机制被称为鸭子类型:如果一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么它就可以被当作一只鸭子。

2.2 接口的定义与隐式实现

package main
import "fmt"
// 1. 定义一个接口
type Speaker interface {
    Speak() string
}
// 2. 定义几个结构体
type Dog struct{}
type Cat struct{}
type Person struct {
    Name string
}
// 3. 为这些结构体实现 Speaker 接口的方法
// Dog 实现了 Speaker 接口
func (d Dog) Speak() string {
    return "Woof!"
}
// Cat 实现了 Speaker 接口
func (c Cat) Speak() string {
    return "Meow!"
}
// Person 实现了 Speaker 接口
func (p Person) Speak() string {
    return "Hello, my name is " + p.Name
}
// 4. 编写一个可以接受任何 Speaker 的函数
func LetItSpeak(s Speaker) {
    fmt.Println(s.Speak())
}
func main() {
    // 创建不同类型的实例
    dog := Dog{}
    cat := Cat{}
    person := Person{Name: "Alice"}
    // 将它们作为 Speaker 接口类型传递给函数
    // 因为 Dog, Cat, Person 都实现了 Speak() 方法,所以它们都实现了 Speaker 接口
    LetItSpeak(dog)    // 输出: Woof!
    LetItSpeak(cat)    // 输出: Meow!
    LetItSpeak(person) // 输出: Hello, my name is Alice
}

2.3 空接口interface{}与类型断言

var i interface{}
i = 42
i = "hello"
i = Dog{}
fmt.Println(i) // 输出: {}
package main
import "fmt"
func main() {
    var i interface{} = "hello, world"
    // 方式1:直接断言,如果断言失败会 panic
    s := i.(string)
    fmt.Println(s) // 输出: hello, world
    // 方式2:安全断言,使用 "ok" 模式
    // 如果断言成功,ok 为 true;如果失败,ok 为 false,str 为该类型的零值
    if str, ok := i.(string); ok {
        fmt.Println("i is a string:", str) // 输出: i is a string: hello, world
    } else {
        fmt.Println("i is not a string")
    }
    // 尝试断言为其他类型
    if num, ok := i.(int); ok {
        fmt.Println("i is an int:", num)
    } else {
        fmt.Println("i is not an int") // 输出: i is not an int
    }
}
func doSomething(i interface{}) {
    switch v := i.(type) {
    case string:
        fmt.Printf("It's a string: %q\n", v)
    case int:
        fmt.Printf("It's an int: %d\n", v)
    case Dog:
        fmt.Printf("It's a Dog: %v\n", v)
    default:
        fmt.Printf("Unknown type: %T\n", v)
    }
}
func main() {
    doSomething("hello")
    doSomething(123)
    doSomething(Dog{})
    doSomething(3.14)
}

2.4 接口的组合

Go 语言的接口也可以像结构体一样进行组合,从而创建出更复杂、更具体的接口。

package main
import "fmt"
// 定义基础接口
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
type File struct {
    name string
}
func (f *File) Read(p []byte) (n int, err error) {
    fmt.Println("Reading from file:", f.name)
    // ... 模拟读取
    return len(p), nil
}
func (f *File) Write(p []byte) (n int, err error) {
    fmt.Println("Writing to file:", f.name)
    // ... 模拟写入
    return len(p), nil
}
func main() {
    file := &File{name: "data.txt"}
    // 因为 File 实现了 Read 和 Write,所以它也实现了 ReadWriter
    var rw ReadWriter = file
    rw.Read([]byte{})
    rw.Write([]byte{})
}

2.5 使用接口的建议

三、综合案例

3.1 计算不同图形的面积和周长

让我们设计一个简单的几何图形系统,计算不同图形的面积和周长。

package main
import (
	"fmt"
	"math"
)
// --- 1. 定义接口 ---
// 定义一个描述几何图形的接口
type Geometry interface {
	Area() float64
	Perimeter() float64
}
// --- 2. 定义结构体 ---
// 定义一个矩形结构体
type Rectangle struct {
	Width, Height float64
}
// 定义一个圆形结构体
type Circle struct {
	Radius float64
}
// --- 3. 为结构体实现接口方法 ---
// Rectangle 实现 Geometry 接口
func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
	return 2 * (r.Width + r.Height)
}
// Circle 实现 Geometry 接口
func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
	return 2 * math.Pi * c.Radius
}
// --- 4. 编写使用接口的函数 ---
// 这个函数不关心传入的是矩形还是圆形,只要它实现了 Geometry 接口即可
func Measure(g Geometry) {
	fmt.Println(g)
	fmt.Printf("Area: %.2f\n", g.Area())
	fmt.Printf("Perimeter: %.2f\n", g.Perimeter())
	fmt.Println("--------------------")
}
// 为了让打印更友好,实现 fmt.Stringer 接口
func (r Rectangle) String() string {
	return fmt.Sprintf("Rectangle (Width: %.2f, Height: %.2f)", r.Width, r.Height)
}
func (c Circle) String() string {
	return fmt.Sprintf("Circle (Radius: %.2f)", c.Radius)
}
// --- 5. 在 main 函数中使用 ---
func main() {
	// 创建具体的图形实例
	r := Rectangle{Width: 10, Height: 5}
	c := Circle{Radius: 7}
	// 将它们作为 Geometry 接口类型传递
	// Measure 函数可以处理任何实现了 Geometry 接口的新类型,无需修改 Measure 函数本身
	// 这就是接口带来的强大扩展性!
	Measure(r)
	Measure(c)
	// 未来如果我们想增加一个三角形,只需定义 Triangle 结构体并实现 Geometry 接口,
	// Measure 函数就能立刻处理它,完美体现了“对扩展开放,对修改关闭”的开闭原则。
}

输出结果:

Rectangle (Width: 10.00, Height: 5.00)
Area: 50.00
Perimeter: 30.00
--------------------
Circle (Radius: 7.00)
Area: 153.94
Perimeter: 43.98
--------------------

到此这篇关于Go语言结构体(Struct)和接口(Interface)详解的文章就介绍到这了,更多相关Go语言结构体和接口内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文