大家好,我是大厂后端程序员阿煜。回望这一路的学习和成长,我深知技术学习过程中的难点与迷茫,希望通过文章让你在技术学习的路上少走弯路,轻松掌握关键知识!
在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、云计算等,如果有需要的小伙伴可以通过私信联系我,免费分享,帮助大家节省时间。