年末最后一更!Go 与 GOF 设计模式(持续更新中 ...)

概览介绍

写在前面

我们尚未正式开始学习,让我们先理智地认识到,设计模式 (Design Patterns) 并非解决所有问题的银弹。确切来说,它们只是一套工具,帮助我们更好地总结经验和最佳实践,以实现优雅且易读的代码。

尽管设计模式具有其重要性,但这并不能补充深入学习和理解数据结构与算法的必要性。在提升代码效率和性能方面,掌握数据结构和算法才是核心关键。这是每一个称职的程序员都必须不断提升和深化的领域。

在实际应用中,设计模式和数据结构、算法是相辅相成的。我们需要结合运用这些工具和知识,才能解决问题并编写出高效、优秀的代码。

此外,持续重构是一个极其重要的过程。通过改善代码的结构和设计,我们方能使代码更加稳健并且易于维护。

因此,在学习设计模式的同时,我们也需要密切关注数据结构和算法的学习。通过不断的实战操作和深化理解,我们会成长为更出色的软件开发者,有能力编写高质量的代码。

学习网站

在线学习网站: Refactoring.Guru

GOF 模式

"GOF" 是指《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)这本经典的设计模式书籍的作者首字母缩写,也被称为 "Gang of Four"。该书由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四位作者合著。

GOF 模式(23 种设计模式)可以按照一些常见的分类方式进行组织,例如 "创建型模式"、"结构型模式" 和 "行为型模式"。这种分类方式可以帮助我们理解和组织这些模式的概念和应用场景,但并不意味着你必须按照特定的顺序学习或使用它们。具体如下:

类型 模式 简单描述
创建型模式 单例模式 确保类只有一个实例,并提供一个全局访问点。
创建型模式 工厂方法模式 定义一个创建对象的接口,但由子类决定要实例化的类是哪一个。
创建型模式 抽象工厂模式 提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
创建型模式 建造者模式 使用简单对象和逐步构建的方式复杂对象进行构建的创建型模式。
创建型模式 原型模式 用于创建对象的种类,并通过拷贝这些原型创建新的对象。
结构型模式 适配器模式 允许对象使用不同的接口进行交互。
结构型模式 桥接模式 将抽象化与实现化解耦,使得二者可以独立变化。
结构型模式 组合模式 将对象组成树形结构以表示"部分-整体"的层次结构。
结构型模式 装饰模式 动态地给一个对象添加一些额外的职责,就增加功能来说,比生成子类更为灵活。
结构型模式 外观模式 为子系统中的一组接口提供一个一致的界面。
结构型模式 享元模式 通过与其他相似对象共享数据来减小内存使用量,或者计算或网络负载。
结构型模式 代理模式 为其他对象提供一个代理以控制对这个对象的访问。
行为型模式 解释器模式 定义语法的一种方式,用于解释一个表达式。
行为型模式 模板方法模式 在不改变模板结构的前提下,重新定义模板的某些特定步骤。
行为型模式 观察者模式 对象之间存在一对多的依赖关系,当一个对象状态改变时,所有依赖于它的对象都会收到通知。
行为型模式 迭代器模式 提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露其内部的表示。
行为型模式 策略模式 定义了一系列的算法,并将每一个算法封装起来,使它们可以互相替换。
行为型模式 命令模式 将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
行为型模式 备忘录模式 在不破坏对象的封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便将来恢复。
行为型模式 访问者模式 主要将数据结构与数据操作分离,解决数据结构和操作之间的耦合性。
行为型模式 中介者模式 用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地互相引用,从而使其耦合松散。
行为型模式 责任链模式 为请求创建了一个接收者对象的链,这些接收者中包含有请求的处理代码。
行为型模式 状态模式 对象的行为依赖于它的状态(即,它的属性),并且可以根据它的状态改变而改变其行为。

创建型模式

单例模式

代码示例

创建了一个Logger类型的单例,并用它来写日志。

go 复制代码
// gof/singleton.go
package gof

import (
    "fmt"
    "sync"
    "time"
)

type Logger struct {
    mu sync.Mutex
}

var instance *Logger
var once sync.Once

func GetInstance() *Logger {
    once.Do(func() {
        instance = &Logger{}
    })
    return instance
}

func (l *Logger) Log(message string, index ...int) {
    l.mu.Lock()
    defer l.mu.Unlock()

    fmt.Printf("[%s] %s %d\n", time.Now().Format("2006-01-02 15:04:05"), message, index[0])
}

这个函数返回的始终是同一个Logger的实例。由sync.Once类型变量once保证的,无论多少协程去调用这个函数,实例都只创建一次。Logger结构体内有一个互斥锁mu,这把锁就是用来保护Log方法的并发访问的。

go 复制代码
// main.go
package main

import (
    "sync"

    "codebase/gof"
)

func main() {
    wg := sync.WaitGroup{}

    for i := 0; i < 100000; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            logger := gof.GetInstance()
            logger.Log("this is a message", i) // 让每一个协程都去写日志
        }(i)
    }

    wg.Wait()
}

客观分析

外界有些言论会说单例模式是一种过时的设计模式。为什么会有这样的观点呢?其实单例模式确实有一些潜在的问题,我们看下面的优劣分析:

类别 内容
优势 1. 全局资源控制:当网络应用需要创建一些全局的对象,其中经常包括数据库连接池、线程池、系统中的配置对象、应用缓存和日志对象等,使用单例模式能够方便地控制其数量,确保系统中只有一个实例存在。 2. 对象创建成本高:如果对象创建的成本较高,例如,它需要消耗大量的系统资源,或者从系统其他部分获取数据,或者进行耗时的计算。在这种情况下,可以通过单例模式防止多次创建相同的对象,节省系统资源。 3. 保持全局一致性:存在一些全局状态需要持续维护,而状态的一致性对系统的运行至关重要。在这种情况下,单例模式可以帮助我们确保全局的一致性。
劣势 1. 违背了"单一职责原则":单一职责原则意味着一个类应该只有一个引起它变化的原因。但在单例模式中,此类既管理对象的创建,又处理业务逻辑,因此当业务逻辑需要修改时,有可能对类的实例化部分造成影响。 2. "隐藏"了类之间的依赖关系:使用单例模式可能会使得依赖关系不再明确,并可能会产生隐藏的依赖关系,这可能会导致代码维护和理解上的困难。 3. 处理并发问题:虽然Java和Go等语言提供了处理并发问题的机制,但在其他不支持这些机制的语言中,需要特别注意并发环境下单例的创建和使用。 4. 难以进行单元测试:由于单例模式在全局只有一个实例,因此当单例对象的状态发生改变时可能影响到其他的测试用例。

作为软件工程师,我们无疑会遇到各种各样关于编程范式和设计模式,包括开篇的单例模式在内的各种评论和观点。陈述有所不同,观点各异,这是必然的。然而,面对各种声音,我们并非要完全放弃使用单例模式。关键在于,我们需要根据特定的应用场景和特殊需求,实施审慎的判断和权衡。每一种设计模式都有其独特的用途和优势,只有明确了这一点,我们才能做出最适宜的决定,从而高效地解决遇到的问题。

工厂方法模式

代码示例

让我们用一个餐馆制作食物的例子来阐释工厂方法模式:

假设你去了一个西餐厅,这个餐厅可以提供各种类型的食物,包括汉堡、比萨、三明治等。当你点单时,你并不知道厨师是如何烹饪这些食物的,你只需告诉服务员你的点单信息。

这里的餐厅相当于工厂,提供的食物相当于产品,点菜动作相当于调用工厂方法。

在代码中,我们可以将其抽象成接口,例如:

go 复制代码
// gof/factory_method.go
package gof

import (
    "fmt"
)

// Food 接口定义了烹饪的共同行为
type Food interface {
    Cook() string
}

// Burger 是汉堡的具体类型
type Burger struct{}

func (b Burger) Cook() string {
    return "Cooking a burger."
}

// Pizza 是披萨的具体类型
type Pizza struct{}

func (p Pizza) Cook() string {
    return "Cooking a pizza."
}

// Kitchen 接口定义了厨房的行为
type Kitchen interface {
    CookFood(order string) Food
}

// Restaurant 实现了 Kitchen 接口
type Restaurant struct{}

func (r Restaurant) CookFood(order string) Food {
    switch order {
    case "burger":
        return &Burger{}
    case "pizza":
        return &Pizza{}
    default:
        fmt.Println("Guest ordered a food that doesn't exist.")
        return nil
    }
}

我们只要知道想吃何种食物,餐厅就会为顾客烹饪相应的食物,顾客并不需要知道烹饪食物的具体细节。

因此,餐厅烹饪食物的过程就是一个典型的工厂方法模式,它把制作食物的过程(创建产品的过程)和顾客消费食物的过程(使用产品的过程)进行了分离。

go 复制代码
// main.go
package main

import (
    "fmt"

    "codebase/gof"
)

func main() {
    restaurant := &gof.Restaurant{}

    food1 := restaurant.CookFood("burger")
    fmt.Println(food1.Cook())

    food2 := restaurant.CookFood("pizza")
    fmt.Println(food2.Cook())

    food3 := restaurant.CookFood("sandwich")
    if food3 != nil {
        fmt.Println(food3.Cook()) // 无效的食物
    }
}

客观分析

工厂方法模式在很多情况下都很有用,但它并不是完美的。以下是一些工厂方法模式的优点和缺点:

类别 内容
优势 1. 代码解耦:客户端代码不需要知道具体的产品类,只需要知道对应的工厂即可。这降低了客户端代码与具体产品类之间的耦合度。 2. 代码可扩展性高:当增加新的产品时,只需要增加新的具体产品类和对应的工厂类,无需修改既有的代码,满足了"开放封闭原则"。 3. 将对象创建集中管理,可以一定程度上控制创建对象的数量和申请的资源,例如连接池、线程池等。
劣势 1. 代码复杂性增加:为每一个产品都要提供一个工厂类,如果产品数量非常多,会导致工厂类的数量也非常多,增加了代码的复杂性。 2. 增加了系统的抽象性和理解难度,对于简单的对象创建,使用工厂方法可能会显得过于复杂。 3. 当产品族中产品类增加的时候,所有的工厂类都需要进行修改,可能会引入错误。

总的来说,工厂方法模式在某些情景下能大大提升代码的可维护性和可扩展性,但是如若应用不当,也可能会导致代码复杂性增加。因此,是否使用工厂方法模式,需要根据具体情况进行选择。

抽象工厂模式

代码示例

让我们再次使用餐馆的例子帮助理解升级版的抽象工厂模式:

假设你去了一个大型综合餐厅,这个餐厅既有西餐厅也有中餐厅,分别提供各类西餐和中餐。当你坐在餐桌旁,你不关心菜品是如何准备的,你只需要点单并享用。这里的各类餐厅就是抽象工厂,菜品就是产品。

在代码层面,你可以抽象出一个食品接口和一个餐厅接口:

go 复制代码
// gof/abstract_factory.go
package gof

// 食物接口
type Food interface {
    Cook() string
}

// 具体的食物:汉堡和披萨
type Burger struct{}

func (b Burger) Cook() string {
    return "Making a burger."
}

type Pizza struct{}

func (p Pizza) Cook() string {
    return "Making a pizza."
}

// 具体的食物:饺子和面条
type Dumpling struct{}

func (b Dumpling) Cook() string {
    return "Making dumplings."
}

type Noodle struct{}

func (p Noodle) Cook() string {
    return "Making noodles."
}

// 餐厅接口
type Restaurant interface {
    MakeMainDish() Food
    MakeSideDish() Food
}

// 西餐厅餐厅(抽象工厂)
type WesternRestaurant struct{}

func (w WesternRestaurant) MakeMainDish() Food {
    return &Burger{}
}
func (w WesternRestaurant) MakeSideDish() Food {
    return &Pizza{}
}

// 中餐厅餐厅(抽象工厂)
type ChineseRestaurant struct{}

func (c ChineseRestaurant) MakeMainDish() Food {
    return &Dumpling{}
}
func (c ChineseRestaurant) MakeSideDish() Food {
    return &Noodle{}
}

这样可以保证当你在中餐厅用餐时,你只能得到中餐,当你在西餐厅用餐时,你只能得到西餐。即,餐厅(抽象工厂)保证给你提供的始终是正确类别的食物(产品)。所以,无论你进入的是哪个类型的餐厅,你都会收到对的主菜和副菜,这就是抽象工厂模式的魅力所在。

go 复制代码
// main.go
package main

import (
    "fmt"

    "codebase/gof"
)

func main() {
    chinese := &gof.ChineseRestaurant{}
    chineseFoodMain := chinese.MakeMainDish().Cook()
    chineseFoodSide := chinese.MakeSideDish().Cook()
    fmt.Println(chineseFoodMain, chineseFoodSide)

    western := &gof.WesternRestaurant{}
    westernFoodMain := western.MakeMainDish().Cook()
    westernFoodSide := western.MakeSideDish().Cook()
    fmt.Println(westernFoodMain, westernFoodSide)
}

客观分析

抽象工厂模式就像是工厂方法模式的进一步延伸和泛化,它的优劣性也更加显著:

类别 内容
优点 1. 分离接口和实现:客户端通过工厂接口创建需要的对象,而不需要关心对象的创建细节。 2. 让更换产品族变得容易:由于抽象工厂类固定提供所有产品类,每个具体工厂类只需确定产品即可,便于系统扩展。 3. 提高灵活性和抗变异性:抽象工厂模式提供的高度抽象性使得系统更具有灵活性和抗变异性,便于应对变动。 4. 它避免了"具体产品"(也就是被创建的对象)与客户代码的耦合。
劣势 1. 不太容易支持新种类的产品:如果需新增不属于现有的产品族,要修改的地方会非常多。 2. 由于涉及到多个工厂类和产品类,所以代码的复杂性会提高。 3. 采用了高级别的抽象,设计和理解起来可能较复杂。

使用抽象工厂模式是需要权衡的,它提供了很大的灵活性,而且可以提高代码的可复用性,但它也可能使你的代码更加复杂,更难以调试和维护。所以选择时需要判断当前的系统架构是否真的需要抽象工厂模式。

非 GoF 模式

GoF 列出了 23 种经典的设计模式,但实际上在编程实践中,有其他一些模式也被程序员广泛接受和使用,主要包括:

设计模式 描述
简单工厂模式(Simple Factory) 并未出现在 GoF 的 23 个设计模式中,但是它的思想非常简单,主要用于创建某类对象,封装了创建对象的逻辑。
空对象模式(Null Object) 空对象替换 NULL 对象实例的检查。一个空对象不是检查空值,而是反应一个不做任何动作的关系。这样的空对象也可以在数据不可用的时候提供默认的行为。
线程池模式(Thread Pool) 这是一种在设计多线程应用程序时常用的模式。一个线程池是一组闲置的线程,这些线程处于等待状态,预备在程序中的其他线程分配给它们的任务去执行。
依赖注入模式(Dependency Injection) 它是一种实现控制反转 IoC (Inversion of Control) 的技术,使得代码更加解耦合。在这种模式中,一个类获得它依赖的对象的引用,而不是自己去构造或找到这些对象。
MVC 模式(Model View Controller) 是一种架构模式,广泛应用于 Web 开发中。
MVVM 模式(Model View ViewModel) 框架模式,用于解决UI开发中的分离问题,如 WPF 和 Angular。

简单工厂模式

"简单工厂模式" 或 "静态工厂模式" (static factory method),是后来由社区提出的一种创建型模式,主要用于创建某一种或某一类对象。在简单工厂模式里,你可以想象,所有的产品都是从同一间工厂流水线上生产出来的,不同的产品只是工厂生产流程的不同分支。

从某些角度来看,简单工厂模式和工厂方法模式是有点相似,因为它们都提供了一种方式来封装对象的创建逻辑。然而,这两种模式的用途和结构存在明显的差异。

我们还是使用餐馆举例,便于理解:

go 复制代码
package main

import "fmt"

// Food 是我们的基本产品接口
type Food interface {
    Cook() string
}

// Burger 是一种具体的食物
type Burger struct {}
func (b Burger) Cook() string {
    return "Making a burger."
}

// Dumpling 也是一种具体的食物
type Dumpling struct {}
func (d Dumpling) Cook() string {
    return "Making dumplings."
}

// FoodFactory 是我们的简单工厂,用来创建食物
func FoodFactory(foodType string) Food {
    switch foodType {
    case "Burger":
        return &Burger{}
    case "Dumpling":
        return &Dumpling{}
    default:
        return nil
    }
}

func main() {
    food1 := FoodFactory("Burger")
    fmt.Println(food1.Cook())
    
    food2 := FoodFactory("Dumpling")
    fmt.Println(food2.Cook())
}

Go 语言中,常会用函数创建并初始化结构体的方式来实现简单工厂模式,这类函数通常会有 "New" 开头的函数名。其实这和简单工厂模式的原理是一样的,Simple Factory 不一定是一个类,很多情况下,它只是一段代码,或者一个函数。

这种 NewXXX 的方式在 Go 语言编程中非常常见。原因有二:其一,Go 语言没有构造函数;其二,结构体的零值不一定是我们想要的初始状态。

go 复制代码
package main

import (
    "fmt"
)

type User struct {
    name string
    age  int
}

func NewUser(name string, age int) *User {
    return &User{name: name, age: age}
}

func main() {
    user := NewUser("John", 20)
    fmt.Println(user) 
}

函数选项模式

函数选项模式(Functional Options),在 Go 社区中普遍得到了独特的喜爱。这个模式为构造函数的调用者提供了极大的灵活性,使得用户可以在保留默认行为的基础上进行任意自定义的配置。

为何 Go + 函数选项才算是绝配?

函数选项模式在 Go 社区的流行,主要归功于其优雅地解决了在 Go 编程中常见的一些问题。

首先,Go 是一门静态类型的语言,且不具备类似 Java 中的构造器重载或 C# 中的命名参数等特性。因此,当一个函数(比如结构体的构造函数)具有许多可选参数时,我们通常需要提供一个包含多字段的配置结构体作为输入。然后,这个配置结构体需要在创建时初始化其所有字段,然而这就带来了一些额外的工作,且使得调用者在理解每个字段具体作用上有所困难。而函数选项模式则提供了一个清晰且简洁的方式来处理这个问题。

其次,Go 的函数是一等公民,并且支持闭包,这使我们可以自然而然地实现函数选项模式。与需要编写大量样板代码的 Builder 模式相比,函数选项模式更加简洁,灵活且易于理解。

最后,函数选项模式由于其灵活性质,也有助于维护 API 的向后兼容性。当你需要增加新的选项时,只需要添加一个新的函数选项即可,无需改变现有的函数签名或是调用代码。

当然,元年后诞生的编程语言基本都支持多编程范式。譬如 Rust,其实也可将函数视为一等公民,并且支持闭包,但由于 Rust 的所有权和生命周期规则,使得在 Rust 中实现函数选项模式可能会相比 Go 中更复杂一些。

至于动态语言,如 PythonJavaScriptPython 有独特的 *args/**kwargs 机制,JS 同样也有 Rest parameters 和解构等方式。这些情况都显示出了,不同的语言,都有着属于自己独特的编程实现方式。

确实可以写,但也属实没必要系列

Python 初始化:

python 复制代码
# Python 版函数选项
class DatabaseConfig:
    def __init__(self, *options):
        self.options = options
        self.host = 'localhost'
        self.port = 3306
        self.user = 'root'
        self.password = 'root'
        self.database = 'test_db'

        for option in options:
            option(self)

def set_host(host):
    def inner(config):
        config.host = host
    return inner

def set_port(port):
    def inner(config):
        config.port = port
    return inner

def set_user(user):
    def inner(config):
        config.user = user
    return inner

def set_password(password):
    def inner(config):
        config.password = password
    return inner

def set_database(database):
    def inner(config):
        config.database = database
    return inner

config = DatabaseConfig(set_host('192.168.1.1'), set_port(8888), set_user('admin'), set_password('admin123'), set_database('my_database'))
print(config.host)      # 输出 192.168.1.1
print(config.port)      # 输出 8888
print(config.user)      # 输出 admin
print(config.password)  # 输出 admin123
print(config.database)  # 输出 my_database
python 复制代码
# 经典的 Python 风格
class DatabaseConfig:
    def __init__(self, host='localhost', port=3306, user='root', password=None, database=None):
        self.host = host
        self.port = port
        self.user = user
        self.password = password
        self.database = database

config = DatabaseConfig(host='192.168.1.1', port=8888, user='admin', password='admin123', database='my_database')
print(config.host)      # 输出 192.168.1.1
print(config.port)      # 输出 8888
print(config.user)      # 输出 admin
print(config.password)  # 输出 admin123
print(config.database)  # 输出 my_database

JavaScript 初始化:

js 复制代码
// Nodejs 版函数选项
class DatabaseConfig {
    constructor(...options) {
        this.options = options;
        this.host = 'localhost';
        this.port = 3306;
        this.user = 'root';
        this.password = 'root';
        this.database = 'test_db';

        options.forEach(option => option(this));
    }
}

const setHost = (host) => (config) => { config.host = host; };
const setPort = (port) => (config) => { config.port = port; };
const setUser = (user) => (config) => { config.user = user; };
const setPassword = (password) => (config) => { config.password = password; };
const setDatabase = (database) => (config) => { config.database = database; };

const config = new DatabaseConfig(
    setHost('192.168.1.1'), 
    setPort(8888), 
    setUser('admin'), 
    setPassword('admin123'), 
    setDatabase('my_database')
);
console.log(config.host);      // Outputs: 192.168.1.1
console.log(config.port);      // Outputs: 8888
console.log(config.user);      // Outputs: admin
console.log(config.password);  // Outputs: admin123
console.log(config.database);  // Outputs: my_database
js 复制代码
// 经典的 ES6 风格
class DatabaseConfig {
    constructor({ host = 'localhost', port = 3306, user = 'root', password = 'root', database = 'test_db' } = {}) {
        this.host = host;
        this.port = port;
        this.user = user;
        this.password = password;
        this.database = database;
    }
}

const config = new DatabaseConfig({
    host: '192.168.1.1', 
    port: 8888, 
    user: 'admin', 
    password: 'admin123', 
    database: 'my_database'
});

console.log(config.host);      // Outputs: 192.168.1.1
console.log(config.port);      // Outputs: 8888
console.log(config.user);      // Outputs: admin
console.log(config.password);  // Outputs: admin123
console.log(config.database);  // Outputs: my_database

最后,来看下最正统的 Go 版本吧

作为一名 Gopher,对于函数选项模式我们有着较深的感情。因此,下面对这个模式进行特别介绍!

go 复制代码
// Golang 函数选项模式
package main

import (
    "fmt"
)

type DatabaseConfig struct {
    host     string
    port     int
    user     string
    password string
    database string
}

type Option func(*DatabaseConfig)
type Chain []Option

func WithHost(host string) Option {
    return func(c *DatabaseConfig) {
        c.host = host
    }
}

func WithPort(port int) Option {
    return func(c *DatabaseConfig) {
        c.port = port
    }
}

func WithUser(user string) Option {
    return func(c *DatabaseConfig) {
        c.user = user
    }
}

func WithPassword(password string) Option {
    return func(c *DatabaseConfig) {
        c.password = password
    }
}

func WithDatabase(database string) Option {
    return func(c *DatabaseConfig) {
        c.database = database
    }
}

func (chain Chain) apply(config *DatabaseConfig) {
    for _, option := range chain {
        option(config)
    }
}

func NewDatabaseConfig(opts ...Option) *DatabaseConfig {
    cfg := &DatabaseConfig{
        host: "localhost",
        port: 3306,
    }
    
    Chain(opts).apply(cfg)

    return cfg
}

func main() {
    config := NewDatabaseConfig(
        WithHost("192.168.1.1"),
        WithPort(8888),
        WithUser("admin"),
        WithPassword("admin123"),
        WithDatabase("my_database"),
    )

    fmt.Printf("Host: %s\nPort: %d\nUser: %s\nPassword: %s\nDatabase: %s\n", config.host, config.port, config.user, config.password, config.database)
}
相关推荐
愤怒的代码11 分钟前
Spring Boot对访问密钥加解密——HMAC-SHA256
java·spring boot·后端
栗豆包27 分钟前
w118共享汽车管理系统
java·spring boot·后端·spring·tomcat·maven
万亿少女的梦16839 分钟前
基于Spring Boot的网络购物商城的设计与实现
java·spring boot·后端
开心工作室_kaic2 小时前
springboot485基于springboot的宠物健康顾问系统(论文+源码)_kaic
spring boot·后端·宠物
0zxm2 小时前
08 Django - Django媒体文件&静态文件&文件上传
数据库·后端·python·django·sqlite
慕城南风9 小时前
Go语言中的defer,panic,recover 与错误处理
golang·go
刘大辉在路上9 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
追逐时光者11 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~12 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·