大厂面试必备系列:Go语言 Interface 深度解析

打印 上一主题 下一主题

主题 1959|帖子 1959|积分 5877

  各人好,我是大厂后端步伐员阿煜。回望这一路的学习和发展,我深知技能学习过程中的难点与迷茫,希望通过文章让你在技能学习的路上少走弯路,轻松把握关键知识!

在Go语言的面试中,关于interface的题目黑白常常见的。很多面试官都会通过考察interface来了解候选人对Go语言特性的把握程度以及编程思维能力。
本日,我们就来深入探讨一下Go语言中的interface并在文章最后附上常晤面试题,让你在面试和现实开发中都能更加得心应手。


什么是interface?

在Go语言里,interface是一种抽象类型,它定义了一组方法的署名,但不包含方法的实现。
换句话说,interface就像是一个契约,它规定了实现这个interface的类型必须提供哪些方法。
任何数据类型,只要实现了接口所有的方法,我们就说它实现了该接口。
怎样定义interface?

我们可以通过type和interface关键字定义出接口:
  1. //定义接口名
  2. type Name interface{
  3.   Method1() //定义方法
  4.   ...
  5. }
复制代码
下面是一个简单的interface定义示例:
  1. package main
  2. import "fmt"
  3. // 定义一个Shape接口 
  4. type Shape interface { 
  5.   Area() float64 
  6.   Perimeter() float64 
  7. }
  8. // 定义一个Rectangle结构体 
  9. type Rectangle struct { 
  10.   Width float64 
  11.   Height float64 
  12. }
  13. // 实现Shape接口的Area方法 
  14. func (r Rectangle) Area() float64 { 
  15.   return r.Width * r.Height 
  16. }
  17. // 实现Shape接口的Perimeter方法 
  18. func (r Rectangle) Perimeter() float64 { 
  19.   return 2 * (r.Width + r.Height) 
  20. }
  21. func main() { 
  22.   rect := Rectangle{Width: 5, Height: 3} 
  23.   var s Shape = rect 
  24.   fmt.Printf("Area: %.2f\n", s.Area()) 
  25.   fmt.Printf("Perimeter: %.2f\n", s.Perimeter()) 
  26. }
复制代码
在上面的代码中,我们定义了一个Shape接口,它包含了两个方法:Area()和Perimeter()。然后我们定义了一个Rectangle结构体,并为它实现了Shape接口的所有方法。
最后,我们创建了一个Rectangle类型的变量rect,并将它赋值给一个Shape类型的变量s,这样就可以通过接口来调用相应的方法了。
用过Java和C++等面向对象语言的小搭档可能第一次见这样的接口实现机制,Go中的接口实现不是通过显式的关键字(例如implement)来实现接口,而是隐式实现
如上面的代码中,Go结构体实现了所有Shape接口的方法,那么就可以说这个结构体实现了Shape接口。
Go为什么这样设计接口?

我们看看官方https://go.dev/doc/faq#implements_interface怎样解释:
  1. A Go type implements an interface by implementing the methods of that interface, nothing more. 
  2. This property allows interfaces to be defined and used without needing to modify existing code. 
  3. 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. 
  4. The semantics of interfaces is one of the main reasons for Go’s nimble, lightweight feel.
复制代码
用简便的话总结一下就是: 定义和利用接口时,不消修改已有代码,从而增长代码可复用性,也使得Go语言感觉更轻量级。
由于是隐式实现机制,任何实现了这些方法的类型都可以被视为实现了该接口,而无需显式声明。这种特性使得代码更加灵活、模块化,而且易于扩展。
例如,我希望给Rectangle结构体增长Printer接口的实现,不需要修改之前的Rectangle的代码,就像挂钩一样,将新的方法“挂”在原来的Rectangle上就行了,将Printer接口的方法都“挂”上去了,就可以说实现了Printer接口。
  1. type interface Printer{
  2.    Output()
  3. }
  4. // 实现Printer接口的Output方法 
  5. func (r Rectangle) Output() { 
  6.   fmt.Printf("The width is : %.2f\n, The Height is : %.2f\n", r.Width,r.Height)
  7. }
复制代码
interface有什么用?

多态

多态是interface最核心的用途之一。通过interface,我们可以实现代码的灵活性和可扩展性。
同一个interface可以有多个不同的实现,我们可以根据不同的需求动态地选择利用哪个实现。
  1. package main
  2. import "fmt"
  3. // 定义一个Animal接口 
  4. type Animal interface { 
  5. Speak() string 
  6. }
  7. // 定义Dog结构体并实现Animal接口 
  8. type Dog struct{}
  9. func (d Dog) Speak() string { 
  10.   return "Woof!" 
  11. }
  12. // 定义Cat结构体并实现Animal接口 
  13. type Cat struct{}
  14. func (c Cat) Speak() string { 
  15.   return "Meow!" 
  16. }
  17. func main() { 
  18.   animals := []Animal{Dog{}, Cat{}} 
  19.   for _, animal := range animals { 
  20.     fmt.Println(animal.Speak()) 
  21.   } 
  22. }
复制代码
在这个例子中,我们定义了一个Animal接口和两个结构体Dog和Cat,它们都实现了Animal接口的Speak()方法。
然后我们创建了一个Animal类型的切片,包含了Dog和Cat的实例,通过循环调用Speak()方法,输出不同动物的叫声,这就是多态的体现。
解耦合代码以及进步代码复用性

interface可以资助我们解耦代码,低沉模块之间的依赖。我们可以通过定义接口来规范不同模块之间的交互,而不需要关心详细的实现细节。
  1. package main
  2. import (
  3.  "fmt"
  4.  "os"
  5. )
  6. // Logger 是一个接口,定义了所有日志记录器必须实现的方法
  7. type Logger interface {
  8.  Log(message string)
  9. }
  10. // ConsoleLogger 实现了 Logger 接口,用于将日志输出到控制台
  11. type ConsoleLogger struct{}
  12. func (c ConsoleLogger) Log(message string) {
  13.  fmt.Println("Console:", message)
  14. }
  15. // FileLogger 实现了 Logger 接口,用于将日志写入文件
  16. type FileLogger struct {
  17.  file *os.File
  18. }
  19. func NewFileLogger(filePath string) (*FileLogger, error) {
  20.  file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
  21.  if err != nil {
  22.   return nil, err
  23.  }
  24.  return &FileLogger{file: file}, nil
  25. }
  26. func (f FileLogger) Log(message string) {
  27.  fmt.Fprintln(f.file, "File:", message)
  28. }
  29. // LogMessage 是一个通用函数,接受任意实现了 Logger 接口的对象
  30. func LogMessage(logger Logger, message string) {
  31.  logger.Log(message)
  32. }
  33. func main() {
  34.  // 使用 ConsoleLogger
  35.  consoleLogger := ConsoleLogger{}
  36.  LogMessage(consoleLogger, "This is a console log.")
  37.  // 使用 FileLogger
  38.  fileLogger, err := NewFileLogger("app.log")
  39.  if err != nil {
  40.   fmt.Println("Error creating file logger:", err)
  41.   return
  42.  }
  43.  defer fileLogger.file.Close()
  44.  LogMessage(fileLogger, "This is a file log.")
  45. }
复制代码
上述代码中,如果将来需要新增其他类型的日志记录器(例如网络日志),只需实现Logger接口即可,无需修改现有代码,进步了代码的复用性。
同时,LogMessage依赖接口,不依赖详细的logger实现,实现了解耦合
interface底层实现

虽然我们在日常开发中不需要过多关注底层实现细节,但了解这些可以资助我们更好地明白interface的工作原理,也资助我们更好的明白相干面试题,知其然且知其所以然~
Go语言的interface底层实现主要涉及两个结构体:iface和eface。
iface:用于表示包含方法的接口。它包含两个指针,一个指向详细类型信息的itab,另一个指向现实的数据。
  1. type iface struct { // 16 字节
  2.  tab  *itab
  3.  data unsafe.Pointer
  4. }
复制代码
eface:用于表示空接口(不包含任何方法的接口)。它也包含两个指针,一个指向类型信息,另一个指向现实的数据。
空接口在现实利用中很常见,所以实现时Go利用了单独的类型。
  1. type eface struct { // 16 字节
  2.  _type *_type
  3.  data  unsafe.Pointer
  4. }
复制代码
从结构体中可以看出,此中只包括类型指针和数据指针,所以任何类型都可以转化为空接口,或者说空接口能表示任何类型。
那么,类型转换的时间会发生什么呢?
  1. package main
  2. type Dog interface {
  3.  Bark()
  4. }
  5. type Cat struct {
  6.  Name string
  7. }
  8. func (c Cat) Bark() {
  9.  println(c.Name + " meow")
  10. }
  11. func main() {
  12.  var c Duck = Cat{Name: "Mimi"}
  13.  c.Bark()
  14. }
复制代码
根据上面的代码来明白,当我们将Cat类型转换为Dog接口类型时,Go编译器会将Cat类型的数据封装为iface结构体,这样接口类型就能通过iface结构体调用原来类型的属性和方法了。
对于接口的底层实现,如果只明白一个点,那就明白到 “类型转换时是存在封装iface或者eface结构体这个过程的!”
明白后,下面的面试题就很简单啦~看题。


  • 判断一下最后的判断语句下面的输出是什么?
  1. type Vehicle interface {
  2.    //空接口
  3. }
  4. type Car struct {
  5.   ...//省略
  6. }
  7. var car1 *Car
  8. fmt.Println("The first car is nil. ")
  9. car2 := car1
  10. fmt.Println("The second car is nil. ")
  11. var vehicle Vehicle = car2
  12. if vehicle == nil {
  13.     fmt.Println("The vehicle is nil. ")
  14. } else {
  15.     fmt.Println("The vehicle is not nil. ")
  16. }
复制代码
当我们了解底层原理之后,应该可以或许轻松的鉴别出最后判断语句输出为"The vehicle is not nil. "。
由于vehicle变量颠末了类型转换,此时变量已经被初始化为内存中的eface结构体,所以不为nil。
常晤面试题

1. 空接口有什么作用?
答:空接口可用于表示通用类型。例如,用在函数的参数或者返回值:
  1. func PrintValue(v interface{})
复制代码
也可以实现耽误类型判断:
  1. var value interface{} = 42
  2. if v, ok := value.(int); ok {
  3.     fmt.Println("It's an int:", v)
  4. }
复制代码
2. interface是值类型还是引用类型?
答:起首,区分值类型和引用类型的最关键点在于数据的存储和传递方式。值类型存储和传递值的现实数据,而引用类型存储和传递的是现实值的地点。
我了解到初始化interface后,Go编译器会在内存中创建iface或者eface结构体,不同的接口变量拥有不同的结构体,所以interface是值类型。
  1. type interface vehicle{
  2.    //空接口
  3. }
  4. type struct Car{
  5.   ...//省略
  6. }
  7. var car1 *Car
  8. var v1, v2 Vehicle
  9. v1 = car1
  10. v2 = v1 // v2 是 v1 的副本
复制代码
3. 当一个类型实现了interface的部分方法,会发生什么?
答:如果一个类型只实现了某个接口的部分方法,而不是全部方法,则该类型不被视为实现了该接口。Go 是静态类型语言,要求类型必须完全实现接口的所有方法才能满足接口的要求。
4. 怎样判断一个接口变量现实指向的详细类型?
答:通过断言判断:value, ok := interfaceVariable.(Type),别的,利用 switch 语句可以根据接口变量的现实类型执行不同的逻辑。
  1. var value interface{} = 42
  2. switch v := value.(type) {
  3. case int:
  4.     fmt.Println("It's an int:", v)
  5. case string:
  6.     fmt.Println("It's a string:", v)
  7. default:
  8.     fmt.Println("Unknown type")
  9. }
复制代码
5. interface的底层实现原理是什么?(见上文)
6. 请形貌一个利用interface解耦代码的场景。(见上文)
总结

通过对Go语言interface的定义、用途、底层实现和常晤面试题的学习,信赖你对这个紧张的概念有了更深入的明白。
在面试和现实开发中,灵活运用interface可以让你的代码更加简便、可维护和具有扩展性。希望各人通过阅读本文,能在Go语言的学习和实践中更加游刃有余~

您可能花了5分钟阅读本片文章,但我却花了5天时间整理、验证和书写,各位小搭档可以帮我点点赞,或者在评论区给我留言讨论,也可以关注我一下,这将是对步伐员阿煜产出优质内容的莫大的鼓励~
关注步伐员阿煜,轻松把握关键知识!
近来整理了一下之前学习的资料,涵盖操纵系统、盘算机网络、AI、云盘算等,如果有需要的小搭档可以通过私信接洽我,免费分享,资助各人节省时间。




免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

莱莱

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表