各人好,我是大厂后端步伐员阿煜。回望这一路的学习和发展,我深知技能学习过程中的难点与迷茫,希望通过文章让你在技能学习的路上少走弯路,轻松把握关键知识!
在Go语言的面试中,关于interface的题目黑白常常见的。很多面试官都会通过考察interface来了解候选人对Go语言特性的把握程度以及编程思维能力。
本日,我们就来深入探讨一下Go语言中的interface并在文章最后附上常晤面试题,让你在面试和现实开发中都能更加得心应手。
什么是interface?
在Go语言里,interface是一种抽象类型,它定义了一组方法的署名,但不包含方法的实现。
换句话说,interface就像是一个契约,它规定了实现这个interface的类型必须提供哪些方法。
任何数据类型,只要实现了接口所有的方法,我们就说它实现了该接口。
怎样定义interface?
我们可以通过type和interface关键字定义出接口:
- //定义接口名
- type Name interface{
- Method1() //定义方法
- ...
- }
复制代码 下面是一个简单的interface定义示例:
- package main
- import "fmt"
- // 定义一个Shape接口
- type Shape interface {
- Area() float64
- Perimeter() float64
- }
- // 定义一个Rectangle结构体
- type Rectangle struct {
- Width float64
- Height float64
- }
- // 实现Shape接口的Area方法
- func (r Rectangle) Area() float64 {
- return r.Width * r.Height
- }
- // 实现Shape接口的Perimeter方法
- func (r Rectangle) Perimeter() float64 {
- return 2 * (r.Width + r.Height)
- }
- func main() {
- rect := Rectangle{Width: 5, Height: 3}
- var s Shape = rect
- fmt.Printf("Area: %.2f\n", s.Area())
- fmt.Printf("Perimeter: %.2f\n", s.Perimeter())
- }
复制代码 在上面的代码中,我们定义了一个Shape接口,它包含了两个方法:Area()和Perimeter()。然后我们定义了一个Rectangle结构体,并为它实现了Shape接口的所有方法。
最后,我们创建了一个Rectangle类型的变量rect,并将它赋值给一个Shape类型的变量s,这样就可以通过接口来调用相应的方法了。
用过Java和C++等面向对象语言的小搭档可能第一次见这样的接口实现机制,Go中的接口实现不是通过显式的关键字(例如implement)来实现接口,而是隐式实现。
如上面的代码中,Go结构体实现了所有Shape接口的方法,那么就可以说这个结构体实现了Shape接口。
Go为什么这样设计接口?
我们看看官方https://go.dev/doc/faq#implements_interface怎样解释:
- A Go type implements an interface by implementing the methods of that interface, nothing more.
- This property allows interfaces to be defined and used without needing to modify existing code.
- It enables a kind of structural typing that promotes separation of concerns and improves code re-use, and makes it easier to build on patterns that emerge as the code develops.
- The semantics of interfaces is one of the main reasons for Go’s nimble, lightweight feel.
复制代码 用简便的话总结一下就是: 定义和利用接口时,不消修改已有代码,从而增长代码可复用性,也使得Go语言感觉更轻量级。
由于是隐式实现机制,任何实现了这些方法的类型都可以被视为实现了该接口,而无需显式声明。这种特性使得代码更加灵活、模块化,而且易于扩展。
例如,我希望给Rectangle结构体增长Printer接口的实现,不需要修改之前的Rectangle的代码,就像挂钩一样,将新的方法“挂”在原来的Rectangle上就行了,将Printer接口的方法都“挂”上去了,就可以说实现了Printer接口。
- type interface Printer{
- Output()
- }
- // 实现Printer接口的Output方法
- func (r Rectangle) Output() {
- fmt.Printf("The width is : %.2f\n, The Height is : %.2f\n", r.Width,r.Height)
- }
复制代码 interface有什么用?
多态
多态是interface最核心的用途之一。通过interface,我们可以实现代码的灵活性和可扩展性。
同一个interface可以有多个不同的实现,我们可以根据不同的需求动态地选择利用哪个实现。
- package main
- import "fmt"
- // 定义一个Animal接口
- type Animal interface {
- Speak() string
- }
- // 定义Dog结构体并实现Animal接口
- type Dog struct{}
- func (d Dog) Speak() string {
- return "Woof!"
- }
- // 定义Cat结构体并实现Animal接口
- type Cat struct{}
- func (c Cat) Speak() string {
- return "Meow!"
- }
- func main() {
- animals := []Animal{Dog{}, Cat{}}
- for _, animal := range animals {
- fmt.Println(animal.Speak())
- }
- }
复制代码 在这个例子中,我们定义了一个Animal接口和两个结构体Dog和Cat,它们都实现了Animal接口的Speak()方法。
然后我们创建了一个Animal类型的切片,包含了Dog和Cat的实例,通过循环调用Speak()方法,输出不同动物的叫声,这就是多态的体现。
解耦合代码以及进步代码复用性
interface可以资助我们解耦代码,低沉模块之间的依赖。我们可以通过定义接口来规范不同模块之间的交互,而不需要关心详细的实现细节。
- package main
- import (
- "fmt"
- "os"
- )
- // Logger 是一个接口,定义了所有日志记录器必须实现的方法
- type Logger interface {
- Log(message string)
- }
- // ConsoleLogger 实现了 Logger 接口,用于将日志输出到控制台
- type ConsoleLogger struct{}
- func (c ConsoleLogger) Log(message string) {
- fmt.Println("Console:", message)
- }
- // FileLogger 实现了 Logger 接口,用于将日志写入文件
- type FileLogger struct {
- file *os.File
- }
- func NewFileLogger(filePath string) (*FileLogger, error) {
- file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if err != nil {
- return nil, err
- }
- return &FileLogger{file: file}, nil
- }
- func (f FileLogger) Log(message string) {
- fmt.Fprintln(f.file, "File:", message)
- }
- // LogMessage 是一个通用函数,接受任意实现了 Logger 接口的对象
- func LogMessage(logger Logger, message string) {
- logger.Log(message)
- }
- func main() {
- // 使用 ConsoleLogger
- consoleLogger := ConsoleLogger{}
- LogMessage(consoleLogger, "This is a console log.")
- // 使用 FileLogger
- fileLogger, err := NewFileLogger("app.log")
- if err != nil {
- fmt.Println("Error creating file logger:", err)
- return
- }
- defer fileLogger.file.Close()
- LogMessage(fileLogger, "This is a file log.")
- }
复制代码 上述代码中,如果将来需要新增其他类型的日志记录器(例如网络日志),只需实现Logger接口即可,无需修改现有代码,进步了代码的复用性。
同时,LogMessage依赖接口,不依赖详细的logger实现,实现了解耦合。
interface底层实现
虽然我们在日常开发中不需要过多关注底层实现细节,但了解这些可以资助我们更好地明白interface的工作原理,也资助我们更好的明白相干面试题,知其然且知其所以然~
Go语言的interface底层实现主要涉及两个结构体:iface和eface。
iface:用于表示包含方法的接口。它包含两个指针,一个指向详细类型信息的itab,另一个指向现实的数据。
- type iface struct { // 16 字节
- tab *itab
- data unsafe.Pointer
- }
复制代码 eface:用于表示空接口(不包含任何方法的接口)。它也包含两个指针,一个指向类型信息,另一个指向现实的数据。
空接口在现实利用中很常见,所以实现时Go利用了单独的类型。
- type eface struct { // 16 字节
- _type *_type
- data unsafe.Pointer
- }
复制代码 从结构体中可以看出,此中只包括类型指针和数据指针,所以任何类型都可以转化为空接口,或者说空接口能表示任何类型。
那么,类型转换的时间会发生什么呢?
- package main
- type Dog interface {
- Bark()
- }
- type Cat struct {
- Name string
- }
- func (c Cat) Bark() {
- println(c.Name + " meow")
- }
- func main() {
- var c Duck = Cat{Name: "Mimi"}
- c.Bark()
- }
复制代码 根据上面的代码来明白,当我们将Cat类型转换为Dog接口类型时,Go编译器会将Cat类型的数据封装为iface结构体,这样接口类型就能通过iface结构体调用原来类型的属性和方法了。
对于接口的底层实现,如果只明白一个点,那就明白到 “类型转换时是存在封装iface或者eface结构体这个过程的!”
明白后,下面的面试题就很简单啦~看题。
- type Vehicle interface {
- //空接口
- }
- type Car struct {
- ...//省略
- }
- var car1 *Car
- fmt.Println("The first car is nil. ")
- car2 := car1
- fmt.Println("The second car is nil. ")
- var vehicle Vehicle = car2
- if vehicle == nil {
- fmt.Println("The vehicle is nil. ")
- } else {
- fmt.Println("The vehicle is not nil. ")
- }
复制代码 当我们了解底层原理之后,应该可以或许轻松的鉴别出最后判断语句输出为"The vehicle is not nil. "。
由于vehicle变量颠末了类型转换,此时变量已经被初始化为内存中的eface结构体,所以不为nil。
常晤面试题
1. 空接口有什么作用?
答:空接口可用于表示通用类型。例如,用在函数的参数或者返回值:
- func PrintValue(v interface{})
复制代码 也可以实现耽误类型判断:
- var value interface{} = 42
- if v, ok := value.(int); ok {
- fmt.Println("It's an int:", v)
- }
复制代码 2. interface是值类型还是引用类型?
答:起首,区分值类型和引用类型的最关键点在于数据的存储和传递方式。值类型存储和传递值的现实数据,而引用类型存储和传递的是现实值的地点。
我了解到初始化interface后,Go编译器会在内存中创建iface或者eface结构体,不同的接口变量拥有不同的结构体,所以interface是值类型。
- type interface vehicle{
- //空接口
- }
- type struct Car{
- ...//省略
- }
- var car1 *Car
- var v1, v2 Vehicle
- v1 = car1
- v2 = v1 // v2 是 v1 的副本
复制代码 3. 当一个类型实现了interface的部分方法,会发生什么?
答:如果一个类型只实现了某个接口的部分方法,而不是全部方法,则该类型不被视为实现了该接口。Go 是静态类型语言,要求类型必须完全实现接口的所有方法才能满足接口的要求。
4. 怎样判断一个接口变量现实指向的详细类型?
答:通过断言判断:value, ok := interfaceVariable.(Type),别的,利用 switch 语句可以根据接口变量的现实类型执行不同的逻辑。
- var value interface{} = 42
- switch v := value.(type) {
- case int:
- fmt.Println("It's an int:", v)
- case string:
- fmt.Println("It's a string:", v)
- default:
- fmt.Println("Unknown type")
- }
复制代码 5. interface的底层实现原理是什么?(见上文)
6. 请形貌一个利用interface解耦代码的场景。(见上文)
总结
通过对Go语言interface的定义、用途、底层实现和常晤面试题的学习,信赖你对这个紧张的概念有了更深入的明白。
在面试和现实开发中,灵活运用interface可以让你的代码更加简便、可维护和具有扩展性。希望各人通过阅读本文,能在Go语言的学习和实践中更加游刃有余~
您可能花了5分钟阅读本片文章,但我却花了5天时间整理、验证和书写,各位小搭档可以帮我点点赞,或者在评论区给我留言讨论,也可以关注我一下,这将是对步伐员阿煜产出优质内容的莫大的鼓励~
关注步伐员阿煜,轻松把握关键知识!
近来整理了一下之前学习的资料,涵盖操纵系统、盘算机网络、AI、云盘算等,如果有需要的小搭档可以通过私信接洽我,免费分享,资助各人节省时间。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |