当前位置: 首页 > news >正文

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

文章目录

    • 一、结构体详解
      • 1.1 什么是结构体?
      • 1.2 结构体的定义与实例化
      • 1.3 结构体的匿名字段与嵌套(组合)
      • 1.4 结构体与方法
    • 二、接口详解
      • 2.1 什么是接口?
      • 2.2 接口的定义与隐式实现
      • 2.3 空接口 `interface{}` 与类型断言
      • 2.4 接口的组合
      • 2.5 使用接口的建议
    • 三、综合案例
      • 3.1 计算不同图形的面积和周长

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 stringLastName  stringAge       intIsActive  bool
}

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

package main
import "fmt"
type Person struct {FirstName stringLastName  stringAge       int
}
func main() {// 方式1:声明一个变量,默认为零值var p1 Personfmt.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 stringAge  intAddress // 匿名字段
}
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.Cityfmt.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 *= factorr.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 指针接收器

  • 使用值接收器:当方法不需要修改接收器,或者接收器是一个较小的结构体时。这更安全,因为不会产生副作用。
  • 使用指针接收器
    1. 当方法需要修改接收器时。
    2. 当接收器是一个大型结构体时,可以避免昂贵的拷贝操作,提高性能。
    3. 为了保证一致性,如果一个结构体的某个方法有指针接收器,那么该结构体的所有方法都应该使用指针接收器。

二、接口详解

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{} 与类型断言

  • 空接口 interface{}:不包含任何方法的接口。因为任何类型都实现了零个方法,所以任何类型都默认实现了空接口。空接口可以存储任意类型的值,类似于 Java 中的 Object 或 C# 中的 object
var i interface{}
i = 42
i = "hello"
i = Dog{}
fmt.Println(i) // 输出: {}
  • 类型断言:由于空接口可以存储任何值,当我们需要从空接口中取出其原始类型的值时,就需要使用类型断言。
package main
import "fmt"
func main() {var i interface{} = "hello, world"// 方式1:直接断言,如果断言失败会 panics := 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 {ReaderWriter
}
// 定义一个结构体来实现 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,所以它也实现了 ReadWritervar rw ReadWriter = filerw.Read([]byte{})rw.Write([]byte{})
}

2.5 使用接口的建议

  • 接受接口,返回结构体:这是一个非常流行的 Go 设计原则。函数的参数应该使用接口类型,以增加灵活性(可以接受任何实现了该接口的类型)。而函数的返回值应该返回具体的结构体类型,以保持明确性(调用者确切地知道得到了什么)。
  • 小接口,大功能:尽量定义包含少量方法(甚至只有一个方法)的接口。这样的接口更容易被实现,也更容易组合。Go 标准库中充满了这样的例子,如 io.Reader, io.Writer, fmt.Stringer
  • 接口属于定义它的包:接口应该由使用者来定义,而不是实现者。这意味着,如果一个包中的函数需要某种行为,它应该定义一个接口来描述这种行为,而不是让实现它的包去定义接口。

三、综合案例

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

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

package main
import ("fmt""math"
)
// --- 1. 定义接口 ---
// 定义一个描述几何图形的接口
type Geometry interface {Area() float64Perimeter() 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
--------------------
http://www.dtcms.com/a/393142.html

相关文章:

  • 【计算机毕业设计】基于生成对抗网络的动作与表情一致性动漫角色生成算法系统​
  • html5 做个人网页识芯平夹回拼翘
  • 开收价均值策略
  • 【大模型部署】Ollama部署gguf模型
  • Coze源码分析-资源库-删除工作流-前端源码-核心组件
  • 机器学习实战第八章 降维
  • 2025年csp-j真题和解析
  • C++ STL map 深度解析:从原理到实战的全方位指南
  • 【EKF组合导航例程】MATLAB代码,15维状态量、3维观测量的组合导航,滤波使用EKF(扩展卡尔曼滤波)。附下载链接
  • word文档怎么根据大纲拆分章节
  • 【Modbus】Modbus协议基础知识详解
  • Springboot使用Integration实现MQTT发送和接收消息
  • 中国传统文化上衣下裳
  • zk管理kafkakafka-broker通信
  • 前端开发技术趋势Web Components
  • Python tarfile库详解
  • ​​[硬件电路-287]:高性能六通道数字隔离器CA-IS3763L 功能概述与管脚定义
  • 错题集系统接口文档
  • 【RAG-LLM】InfoGain-RAG基于文档信息增益的RAG
  • Browser-Use深度解析:重新定义AI与浏览器的智能协作
  • 【Mysql】事务隔离级别、索引原理、/redolog/undolog/binlog区别、主从复制原理
  • AWS 全景速查手册
  • 小米Openvela城市沙龙
  • Python数据分析:求矩阵的秩。啥是矩阵秩?听故事学线代并用Python实现,娘来太容易学会了!
  • UI Toolkit自定义元素
  • redis未授权访问-漏洞复现
  • PR调节器与PI调节器的区别
  • Unity核心概念⑫:碰撞检测
  • 【读论文】面向工业的ASR语音大模型
  • 重谈IO——五种IO模型及其分类