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

56 阅读9分钟

大家好,我是大厂后端程序员阿煜。回望这一路的学习和成长,我深知技术学习过程中的难点与迷茫,希望通过文章让你在技术学习的路上少走弯路,轻松掌握关键知识!


在Go语言的面试中,关于interface的问题是非常常见的。很多面试官都会通过考察interface来了解候选人对Go语言特性的掌握程度以及编程思维能力。

今天,我们就来深入探讨一下Go语言中的interface并在文章最后附上常见面试题,让你在面试和实际开发中都能更加得心应手。

什么是interface?

在Go语言里,interface是一种抽象类型,它定义了一组方法的签名,但不包含方法的实现。

换句话说,interface就像是一个契约,它规定了实现这个interface的类型必须提供哪些方法。

任何数据类型,只要实现了接口所有的方法,我们就说它实现了该接口。

如何定义interface?

我们可以通过typeinterface关键字定义出接口:

//定义接口名
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 interfacenothing 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接口和两个结构体DogCat,它们都实现了Animal接口Speak()方法。

然后我们创建了一个Animal类型的切片,包含了DogCat的实例,通过循环调用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底层实现主要涉及两个结构体:ifaceeface

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、云计算等,如果有需要的小伙伴可以通过私信联系我,免费分享,帮助大家节省时间。