1. 用golang写一个线程安全的单例(Singleton)类
go
package main
import (
"fmt"
"sync"
)
type Singleton struct {
data string
}
var instance *Singleton
var one sync.Once
// 返回单例实例
func GetInstance() *Singleton {
one.Do(func() {
instance = &Singleton{data: "初始化数据...."}
})
return instance
}
func main() {
s1 := GetInstance()
s2 := GetInstance()
fmt.Println(s1 == s2) // 两个实例是相等的
// 验证单例数据
fmt.Println(s1.data)
s2.data = "修改后的数据..."
fmt.Println(s1.data)
}
2. 假设你工作的系统不支持事务性,你会如何从头开始实现它?
go
package main
import (
"fmt"
"sync"
)
// Operation 是一个接口,定义了执行和回滚操作
type Operation interface {
Execute() error
Rollback() error
}
// WriteOperation 是一个简单的写操作,包含目标和数据
type WriteOperation struct {
target *string
data string
oldData string
}
func (op *WriteOperation) Execute() error {
op.oldData = *op.target
*op.target = op.data
return nil
}
func (op *WriteOperation) Rollback() error {
*op.target = op.oldData
return nil
}
// Transaction 管理一组操作
type Transaction struct {
operations []Operation
}
func (t *Transaction) AddOperation(op Operation) {
t.operations = append(t.operations, op)
}
func (t *Transaction) Commit() error {
for _, op := range t.operations {
if err := op.Execute(); err != nil {
t.Rollback()
return err
}
}
return nil
}
func (t *Transaction) Rollback() {
for i := len(t.operations) - 1; i >= 0; i-- {
t.operations[i].Rollback()
}
}
func main() {
var wg sync.WaitGroup
data1 := "initial1"
data2 := "initial2"
// 创建第一个事务
t1 := &Transaction{}
t1.AddOperation(&WriteOperation{target: &data1, data: "data1_1"})
t1.AddOperation(&WriteOperation{target: &data2, data: "data1_2"})
// 创建第二个事务
t2 := &Transaction{}
t2.AddOperation(&WriteOperation{target: &data1, data: "data2_1"})
t2.AddOperation(&WriteOperation{target: &data2, data: "data2_2"})
// 启动第一个事务
wg.Add(1)
go func() {
defer wg.Done()
if err := t1.Commit(); err != nil {
fmt.Println("Transaction 1 failed:", err)
} else {
fmt.Println("Transaction 1 succeeded")
}
}()
// 启动第二个事务
wg.Add(1)
go func() {
defer wg.Done()
if err := t2.Commit(); err != nil {
fmt.Println("Transaction 2 failed:", err)
} else {
fmt.Println("Transaction 2 succeeded")
}
}()
wg.Wait()
fmt.Println("Final data1:", data1)
fmt.Println("Final data2:", data2)
}
3. 创建一个有害的全局对象并说明 问题 并修复它
go
package main
import (
"fmt"
)
// 全局对象
var config = map[string]string{
"mode": "development",
}
// 修改配置的函数
func setMode(mode string) {
config["mode"] = mode
}
// 获取配置的函数
func getMode() string {
return config["mode"]
}
func main() {
fmt.Println("Initial mode:", getMode())
// 在其他地方修改配置
setMode("production")
fmt.Println("Mode after change:", getMode())
// 假设在另一个地方依赖于配置的某个函数
runService()
}
func runService() {
// 假设这个函数依赖于配置的 mode
mode := getMode()
if mode == "production" {
// 这里打印了mode1 全局对象被改变了,这可能会导致意外的副作用和难以追踪的错误。
fmt.Println("Running in production mode1")
} else {
fmt.Println("Running in development mode2")
}
}
4. golang中空对象模式(Null Object Pattern)的目的是什么?
空对象模式的目的和好处
- 减少空值检查:通过使用空对象,可以减少或消除代码中对空值的显式检查,从而使代码更清晰。
- 提供默认行为:空对象可以提供默认的行为,避免在处理空值时出现的空指针异常(nil pointer dereference)。
- 统一接口:空对象实现了与其他对象相同的接口,确保了代码的一致性和可替换性。
- 简化代码逻辑:使用空对象可以简化业务逻辑中的条件分支,减少代码复杂性。
例子
go
package main
import "fmt"
// Logger 是一个日志记录器接口
type Logger interface {
Info(message string)
Error(message string)
}
// ConsoleLogger 是一个具体的日志记录器实现,将日志打印到控制台
type ConsoleLogger struct{}
func (l *ConsoleLogger) Info(message string) {
fmt.Println("INFO:", message)
}
func (l *ConsoleLogger) Error(message string) {
fmt.Println("ERROR:", message)
}
// NullLogger 是一个空日志记录器实现,不执行任何操作
type NullLogger struct{}
func (l *NullLogger) Info(message string) {}
func (l *NullLogger) Error(message string) {}
func main() {
// 使用 ConsoleLogger
var logger Logger = &ConsoleLogger{}
logger.Info("This is an info message")
logger.Error("This is an error message")
// 使用 NullLogger
logger = &NullLogger{}
logger.Info("This message will not be logged")
logger.Error("This error will not be logged")
}
5. 为什么组合(Composition)比继承(Inheritance)更好?
在软件设计中,组合(Composition)通常被认为比继承(Inheritance)更好,主要原因包括以下几个方面:
松耦合:
继承导致子类和父类之间的紧密耦合,子类的实现依赖于父类的实现,任何对父类的修改都可能影响到子类。
组合通过将行为委托给独立的对象,使类之间的耦合度降低。组合允许对象在运行时被替换,从而提高了系统的灵活性和可维护性。
灵活性:
继承是一种静态关系,在编译时决定,并且一个类只能继承一个父类(在Go中是这样)。这限制了类的扩展方式。
组合允许将多个独立的功能组合在一起,通过组合不同的组件,可以动态地创建新的行为和功能。
代码重用:
继承往往导致代码重复和冗余,因为子类会继承父类的所有行为,即使某些行为对子类是不必要的。
组合通过将通用功能提取到独立的组件中,允许这些组件在不同的类中重用,从而减少代码重复。
遵循设计原则:
组合更符合"组合优于继承"的设计原则(Composition over Inheritance)以及单一职责原则(Single Responsibility Principle),即每个类应该只有一个职责。
继承容易导致违反单一职责原则,因为子类会继承父类的所有行为,这些行为可能与子类的职责不完全匹配。
避免继承层次复杂性:
随着继承层次的加深,代码的复杂性和维护成本会显著增加,子类必须理解和维护整个继承链的行为。
组合通过将功能模块化,避免了复杂的继承层次,从而简化了系统的设计和维护。
go
package main
// 继承
import "fmt"
// Animal 是一个基类,定义了动物的通用行为
type Animal struct{}
func (a *Animal) Eat() {
fmt.Println("Animal is eating")
}
// Dog 继承自 Animal,并添加了新的行为
type Dog struct {
Animal
}
func (d *Dog) Bark() {
fmt.Println("Dog is barking")
}
func main() {
dog := &Dog{}
dog.Eat() // 从 Animal 继承的方法
dog.Bark() // Dog 自己的方法
}
go
package main
// 组合
import "fmt"
// Eater 接口定义了吃的行为
type Eater interface {
Eat()
}
// Barker 接口定义了叫的行为
type Barker interface {
Bark()
}
// Animal 实现了 Eater 接口
type Animal struct{}
func (a *Animal) Eat() {
fmt.Println("Animal is eating")
}
// Dog 通过组合 Animal 和 Barker 实现了所需的行为
type Dog struct {
Eater
Barker
}
// DogBarker 实现了 Barker 接口
type DogBarker struct{}
func (b *DogBarker) Bark() {
fmt.Println("Dog is barking")
}
func main() {
animal := &Animal{}
barker := &DogBarker{}
dog := &Dog{
Eater: animal,
Barker: barker,
}
dog.Eat() // 通过组合的 Animal 的方法
dog.Bark() // 通过组合的 DogBarker 的方法
}
6 你是如何处理依赖关系地狱(Dependency Hell)的?
在 Go 中,依赖关系地狱(Dependency Hell)通常指的是由于项目中依赖的外部包或库过多,版本管理和兼容性问题导致的各种麻烦。为了解决这些问题,Go 提供了一些工具和最佳实践来管理依赖关系。
- 使用 Go Modules
- 使用语义版本控制(Semantic Versioning)
- 使用 Go Modules 代理
- 版本锁定(Vendoring)
- 保持依赖项的最小化
- 定期更新和审查依赖项