Golang 依赖注入设计哲学|12.6K 的依赖注入库 wire
一、前言
线上项目往往依赖非常多的具备特定能力的资源,如:DB、MQ、各种中间件,以及随着项目业务的复杂化,单一项目内,业务模块也逐渐增多,如何高效、整洁管理各种资源十分重要。
本文从“术”层面,讲述“依赖注入”的实现,带你体会其对于整洁架构 & DDD 等设计思想的落地,起到的支撑作用。
涉及内容:
-
最热门的 golang 依赖注入库,GitHub 🌟 12.5k:https://github.com/google/wire
-
GiuHub 🌟 22.5k 的 golang 微服务框架 kratos 默认使用 wire 作为依赖注入方式:https://github.com/go-kratos/kratos
-
Spring Boot 与 Golang 的依赖注入对比
-
依赖注入的设计哲学
📺 B站账号:白泽talk,绝大部分博客内容都将会通过视频讲解,不过文章一般是先于视频发布
白泽的开源 Golang 学习仓库:https://github.com/BaiZe1998/go-learning,用于文章归档 & 聚合博客代码案例
公众号【白泽talk】,本期内容的 pdf 版本,可以关注公众号,回复【依赖注入】获得,往期资源的获取,都是类似的方式。
二、What
📒 本文所涉及编写的代码,已收录于 https://github.com/BaiZe1998/go-learning/di 目录
一句话概括:实例 A 的创建,依赖于实例 B 的创建,且在实例 A 的生命周期内,持有对实例 B 的访问权限。
2.1 案例分析
依赖注入(Dependency Injection, DI),以 Golang 为例,左侧为手动完成依赖注入,右侧为不使用依赖注入:
🌟 不使用依赖注入风险:
- 全局变量十分不安全,存在覆写的可能
- 资源散落在各处,可能重复创建,浪费内存,后续维护能力极差
- 提高循环依赖的风险
- 全局变量的引入提高单元测试的成本

- 不使用依赖注入 demo
package mainvar (mysqlUrl = "mysql://blabla"// 全局数据库实例db = NewMySQLClient(mysqlUrl)
)func NewMySQLClient(url string) *MySQLClient {return &MySQLClient{url: url}
}type MySQLClient struct {url string
}func (c *MySQLClient) Exec(query string, args ...interface{}) string {return "data"
}func NewApp() *App {return &App{}
}type App struct {
}func (a *App) GetData(query string, args ...interface{}) string {data := db.Exec(query, args...)return data
}// 不使用依赖注入
func main() {app := NewApp()rest := app.GetData("select * from table where id = ?", "1")println(rest)
}
- 手动依赖注入 demo
package mainfunc NewMySQLClient(url string) *MySQLClient {return &MySQLClient{url: url}
}type MySQLClient struct {url string
}func (c *MySQLClient) Exec(query string, args ...interface{}) string {return "data"
}func NewApp(client *MySQLClient) *App {return &App{client: client}
}type App struct {// App 持有唯一的 MySQLClient 实例client *MySQLClient
}func (a *App) GetData(query string, args ...interface{}) string {data := a.client.Exec(query, args...)return data
}// 手动依赖注入
func main() {client := NewMySQLClient("mysql://blabla")app := NewApp(client)rest := app.GetData("select * from table where id = ?", "1")println(rest)
}
三、Why
依赖注入 (Dependency Injection,缩写为 DI),可以理解为一种代码的构造模式(就是写法),按照这样的方式来写,能够让你的代码更加容易维护。
四、How
4.1 Golang 依赖注入
以 Golang 🌟 最多的开源库 wire 为例讲解:https://github.com/google/wire/blob/main/docs/guide.md
wire是由 google 开源的一个供 Go 语言使用的依赖注入代码生成工具。它能够根据你的代码,生成相应的依赖注入 go 代码。
而与其它依靠反射实现的依赖注入工具不同的是,wire 能在编译期(准确地说是代码生成时)如果依赖注入有问题,在代码生成时即可报出来,不会拖到运行时才报,更便于 debug。
- Install:
go install github.com/google/wire/cmd/wire@latest
- provider: a function that can produce a value
以上面手动实现依赖注入为基础,wire 做的工作是帮助开发者完成如下组装过程
client := NewMySQLClient("mysql://blabla")
app := NewApp(client)
而其中用到的 NewMySQLClient、NewApp 在 wire 定义为一个个的 provider,是需要提前由开发者实现的。
func NewMySQLClient(url string) *MySQLClient {return &MySQLClient{url: url}
}func NewApp(client *MySQLClient) *App {return &App{client: client}
}
假设系统中的资源很多,配置很多,出现了如下复杂的初始化流程,人工完成依赖注入则变得复杂:
a := NewA(xxx, yyy) error
b := NewB(ctx, a) error
c := NewC(zzz, a, b) error
d := NewD(www, kkk, a) error
e := NewD(ctx, b, d) error
- injector: a function that calls providers in dependency order
如下是名为 wire.go 的依赖注入配置文件,是一个只会被 wire 命令行工具处理的 injector 文件,用于声明依赖注入流程。
wire.go:
//go:build wireinject
// +build wireinject// The build tag makes sure the stub is not built in the final build.package mainimport "github.com/google/wire"// wireApp init application.
func wireApp(url string) *App {wire.Build(NewMySQLClient, NewApp)return nil
}
执行 wire 命令,则在当前目录下生成 wire_gen.go 文件,此时的 wireApp 函数,就等价于最初手动编写的依赖注入流程,可以在真正需要初始化的引入。
wire_gen.go:
// Code generated by Wire. DO NOT EDIT.//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinjectpackage main// Injectors from wire.go:// wireApp init application.
func wireApp(url string) *App {mySQLClient := NewMySQLClient(url)app := NewApp(mySQLClient)return app
}
4.2 针对复杂项目的依赖注入设计哲学
这里以 go-kratos 的模版项目为例讲解,是一个 helloworld 服务,我们着重分析其借助 wire 进行依赖注入的部分。
以下 helloworld 模板服务的 interanl 目录的内容:
.
├── biz
│ ├── README.md
│ ├── biz.go
│ └── greeter.go
├── conf
│ ├── conf.pb.go
│ └── conf.proto
├── data
│ ├── README.md
│ ├── data.go
│ └── greeter.go
├── server
│ ├── grpc.go
│ ├── http.go
│ └── server.go
└── service├── README.md├── greeter.go└── service.go
各个目录的关系如图:

-
data:业务数据访问,包含 cache、db 等封装,实现了 biz 的 repo 接口,data 偏重业务的含义,它所要做的是将领域对象重新拿出来。
-
biz:业务逻辑的组装层,类似 DDD 的 domain 层,data 类似 DDD 的 repo,repo 接口在这里定义,使用依赖倒置的原则。
-
service:实现了 api 定义的服务层,类似 DDD 的 application 层,处理 DTO 到 biz 领域实体的转换(DTO -> DO),同时协同各类 biz 交互,但是不应处理复杂逻辑。
-
server:为http和grpc实例的创建和配置,以及注册对应的 service 。
🌟上图右侧部分,表示了模块之间的依赖关系,可以看到,依赖的注入是逆向的,资源往往被业务模块持有,业务模块则被负责编排业务的应用持有,应用则被负责对外通信的模块持有。
此时在服务启动前的实例化阶段,provider 的定义和注入,本质是这样一种状态:
func main() {dbClient := NewDBClient()dataN := NewDataN(dbClient)dataM := NewDataM(dbClient)bizA := NewBizA(dataN)bizB := NewBizB(dataM)bizC := NewBizC(dataN, dataM)serviceX := NewService(bizA, bizB, bizC)server := NewServer(serviceX)server.httpXXX // 提供 http 服务server.grpcXXX // 提供 grpc 服务
}
在 helloworld 这个 demo 当中,则是这样定义 provider 的:
// biz 目录
var ProviderSet = wire.NewSet(NewGreeterUsecase)type GreeterUsecase struct {repo GreeterRepolog *log.Helper
}func NewGreeterUsecase(repo GreeterRepo, logger log.Logger) *GreeterUsecase {return &GreeterUsecase{repo: repo, log: log.NewHelper(logger)}
}func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)return uc.repo.Save(ctx, g)
}// data 目录
var ProviderSet = wire.NewSet(NewData, NewGreeterRepo)type Data struct {// TODO wrapped database client
}func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {cleanup := func() {log.NewHelper(logger).Info("closing the data resources")}return &Data{}, cleanup, nil
}type greeterRepo struct {data *Datalog *log.Helper
}func NewGreeterRepo(data *Data, logger log.Logger) biz.GreeterRepo {return &greeterRepo{data: data,log: log.NewHelper(logger),}
}
// service 目录
var ProviderSet = wire.NewSet(NewGreeterService)type GreeterService struct {v1.UnimplementedGreeterServeruc *biz.GreeterUsecase
}func NewGreeterService(uc *biz.GreeterUsecase) *GreeterService {return &GreeterService{uc: uc}
}func (s *GreeterService) SayHello(ctx context.Context, in *v1.HelloRequest) (*v1.HelloReply, error) {g, err := s.uc.CreateGreeter(ctx, &biz.Greeter{Hello: in.Name})if err != nil {return nil, err}return &v1.HelloReply{Message: "Hello " + g.Hello}, nil
}// server 目录
var ProviderSet = wire.NewSet(NewGRPCServer, NewHTTPServer)func NewGRPCServer(c *conf.Server, greeter *service.GreeterService, logger log.Logger) *grpc.Server {var opts = []grpc.ServerOption{grpc.Middleware(recovery.Recovery(),),}if c.Grpc.Network != "" {opts = append(opts, grpc.Network(c.Grpc.Network))}if c.Grpc.Addr != "" {opts = append(opts, grpc.Address(c.Grpc.Addr))}if c.Grpc.Timeout != nil {opts = append(opts, grpc.Timeout(c.Grpc.Timeout.AsDuration()))}srv := grpc.NewServer(opts...)v1.RegisterGreeterServer(srv, greeter)return srv
}
在 helloworld 这个 demo 当中,则是这样定义 injector 的:
// wire.go
func wireApp(*conf.Server, *conf.Data, log.Logger) (*kratos.App, func(), error) {panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}
最后运行 wire 的到的完成注入的文件如下:
// wire_gen.go
func wireApp(confServer *conf.Server, confData *conf.Data, logger log.Logger) (*kratos.App, func(), error) {dataData, cleanup, err := data.NewData(confData, logger)if err != nil {return nil, nil, err}greeterRepo := data.NewGreeterRepo(dataData, logger)greeterUsecase := biz.NewGreeterUsecase(greeterRepo, logger)greeterService := service.NewGreeterService(greeterUsecase)grpcServer := server.NewGRPCServer(confServer, greeterService, logger)httpServer := server.NewHTTPServer(confServer, greeterService, logger)app := newApp(logger, grpcServer, httpServer)return app, func() {cleanup()}, nil
}
生成代码之后,则可以像使用普通的 golang 函数一样,使用这个 wire_gen.go 文件内的 wireApp 函数实例化一个 helloworld 服务
func main() {flag.Parse()logger := log.With(log.NewStdLogger(os.Stdout),// ...)c := config.New(// ...)defer c.Close()// ...app, cleanup, err := wireApp(bc.Server, bc.Data, logger)if err != nil {panic(err)}defer cleanup()// start and wait for stop signalif err := app.Run(); err != nil {panic(err)}
}
4.3 wire 的更多用法
参见 wire 的文档,自己用几遍就明白了,这里举几个例子:
- 定义携带 error 返回值的 provider
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {if bar.X == 0 {return Baz{}, errors.New("cannot provide baz when bar is zero")}return Baz{X: bar.X}, nil
}
- provider 集合:方便组织多个 provider
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)
- 接口绑定:
type Fooer interface {Foo() string
}type MyFooer stringfunc (b *MyFooer) Foo() string {return string(*b)
}func provideMyFooer() *MyFooer {b := new(MyFooer)*b = "Hello, World!"return b
}type Bar stringfunc provideBar(f Fooer) string {// f will be a *MyFooer.return f.Foo()
}var Set = wire.NewSet(provideMyFooer,wire.Bind(new(Fooer), new(*MyFooer)),provideBar)
五、对比 Spring Boot 的依赖注入
Spring Boot的依赖注入(DI)和Golang开源库Wire的依赖注入在设计思路上存在一些相同点和不同点。以下是对这些相同点和不同点的分析:
相同点
- 降低耦合度:两者都通过依赖注入的方式实现了代码的松耦合。这意味着,一个对象不需要显式地创建或查找它所依赖的其他对象,这些依赖项会由外部容器(如Spring容器)或工具(如Wire)自动提供。
- 提高可测试性:由于依赖关系被解耦,可以更容易地替换依赖项以进行单元测试。无论是Spring Boot还是使用Wire的Golang应用,都可以轻松地为组件提供模拟或存根的依赖项以进行测试。
- 灵活性:两者都允许在不修改组件代码的情况下替换依赖项。这使得应用程序在维护和扩展时更加灵活。
不同点
- 实现方式:
- Spring Boot的依赖注入是基于Java的反射机制和Spring框架的容器管理功能实现的。Spring容器负责创建和管理Bean的生命周期,并在需要时自动注入依赖项,核心在于运行时。
- Wire是一个Golang的代码生成工具,它通过分析代码中的构造函数和结构体标签,自动生成依赖注入的代码(减少人工工作量),在开发阶段已经通过工具生成好了依赖注入的代码,程序编译时,资源之间的依赖关系已经固定。
- 配置方式:
- Spring Boot的依赖注入通常通过配置文件(如application.properties或application.yml)和注解(如@Autowired)进行配置。开发者可以在配置文件中定义Bean的属性,并通过注解在需要注入的地方指明依赖关系。
- Wire则通过特殊的Go文件(通常是wire.go文件)来定义类型之间的依赖关系。这些文件包含了用于生成依赖注入代码的指令和元数据。
- 运行时开销:
- Spring Boot的依赖注入在运行时需要依赖Spring容器来管理Bean的生命周期和依赖关系。这可能会引入一些额外的运行时开销,特别是在大型应用程序中。
- Wire在编译时生成依赖注入的代码,因此它在运行时没有额外的开销。这使得使用Wire的Golang应用程序通常具有更好的性能。
六、参考资料
kratos:https://go-kratos.dev/en/docs/getting-started/start/
wire:https://github.com/google/wire/blob/main/_tutorial/README.md
相关文章:
Golang 依赖注入设计哲学|12.6K 的依赖注入库 wire
一、前言 线上项目往往依赖非常多的具备特定能力的资源,如:DB、MQ、各种中间件,以及随着项目业务的复杂化,单一项目内,业务模块也逐渐增多,如何高效、整洁管理各种资源十分重要。 本文从“术”层面&#…...
ubuntu 23 连接正点imx6ull的uboot网络设置(nfs和tftp)
由于使用ubuntu23,无法连接正点的imx6ull的uboot,因为这个uboot里面的nfs是v2,ubuntu23内核是6.5不支持uboot v2。配置/etc/default/nfs-kernel-server sudo vim /etc/default/nfs-kernel-server 更改以下参数: RPCNFSDCOUNT"…...
CC6利用链分析
CC1的两条利用链,在JDK 8u71之后已修复,不可利用。 学一下不受版本限制的CC6利用链 分析版本 Commons Collections 3.2.1 JDK 8u65 环境配置参考JAVA安全初探(三):CC1链全分析 分析过程 我的Github主页Java反序列化学习同步更新,有简单…...
多线程编程的基本概念,C++标准库中的多线程支持(std::thread,std::async),如何处理线程同步和并发问题。
多线程编程在现代计算机系统中非常重要,因为它能够使程序同时执行多个操作,提高计算效率。以下是多线程编程的基本概念及如何在C标准库中使用std::thread和std::async进行多线程编程,同时处理线程同步和并发问题。 多线程编程的基本概念 线程…...
Linux的Socket开发概述
套接字(socket)是 Linux 下的一种进程间通信机制(socket IPC),在前面的内容中已经给大家提到过,使用 socket IPC 可以使得在不同主机上的应用程序之间进行通信(网络通信),…...
LLM调优,大模型怎么学
背景 LLM Transparency Tool 是一个用于深入分析和理解大型语言模型(LLM)工作原理的工具,旨在增加这些复杂系统的透明度。它提供了一个交互式界面,用户可以通过它观察、分析模型对特定输入(prompts)的反应…...
XLSX + LuckySheet + LuckyExcel实现前端的excel预览
文章目录 功能简介简单代码实现效果参考 功能简介 通过LuckyExcel的transformExcelToLucky方法, 我们可以把一个文件直接转成LuckySheet需要的json字符串, 之后我们就可以用LuckySheet预览excelLuckyExcel只能解析xlsx格式的excel文件,因此对…...
在Ubuntu上创建和启用交换文件的简单步骤
文章目录 为什么使用交换文件?步骤 1:创建交换文件步骤 2:设置正确的权限步骤 3:将文件格式化为交换空间步骤 4:启用交换文件步骤 5:验证交换文件步骤 6:永久启用交换文件步骤 7:调整…...
Java [ 基础 ] HashMap详解 ✨
目录 ✨探索Java基础 HashMap详解✨ 总述 主体 1. HashMap的基本概念 2. HashMap的工作原理 3. HashMap的常用操作 4. HashMap的优缺点 总结 常见面试题 常见面试题解答 1. HashMap的底层实现原理是什么? 2. 如何解决HashMap中的哈希冲突?…...
vue2项目迁移vue3与gogocode的使用
#背景 公司有个项目使用vue2jswebpack框架开发的,由于该项目内部需要安扫,导致很多框架出现了漏洞需要升级,其中主要需要从vue2升vue3,但是重新搭框架推翻重做成本太高,于是找到了gogocode。 #升级步骤踩坑 1. 安装 gogocode插…...
【Python123题库】#数列求和 #百分制成绩转换五分制(循环) #正负交错数列前n项和 #求数列前n项的平方和
禁止转载,原文:https://blog.csdn.net/qq_45801887/article/details/140079866 参考教程:B站视频讲解——https://space.bilibili.com/3546616042621301 有帮助麻烦点个赞 ~ ~ Python123题库 数列求和百分制成绩转换五分制(循环)正负交错数列…...
Edge浏览器选中后,出现AI智能生成 AI专业写作
这个是扩展里边的“ 网页万能复制 & ChatGPT AI写作助手”造成的,这个拓展增加了AI写作功能。关闭这个拓展就解决了。...
c++习题08-计算星期几
目录 一,问题 二,思路 三,代码 一,问题 二,思路 首先,需要注意到的是3^2000这个数值很大,已经远远超过了long long 数据类型能够表示的范围,如果想要使用指定的数据类型来保存…...
单目相机减速带检测以及测距
单目相机减速带检测以及测距项目是一个计算机视觉领域的应用,旨在使用一个摄像头(单目相机)来识别道路上的减速带,并进一步估计车辆与减速带之间的距离。这样的系统对于智能驾驶辅助系统(ADAS)特别有用&…...
Xilinx FPGA:vivado实现乒乓缓存
一、项目要求 1、用两个伪双端口的RAM实现缓存 2、先写buffer1,再写buffer2 ,在读buffer1的同时写buffer2,在读buffer2的同时写buffer1。 3、写端口50M时钟,写入16个8bit 的数据,读出时钟25M,读出8个16…...
解决 VM 虚拟机网络连接异常导致的 Finalshell 无法连接及 ifconfig 中 ens33 丢失问题
在使用 VM 虚拟机的过程中,遇到了一个颇为棘手的网络连接问题。平时虚拟机都能够正常启动并使用,但昨天在启用虚拟机时更换了一下网络节点,结果今天打开虚拟机后。Finalshell 无法连接上虚拟机,并且输入 ifconfig 命令后也没有 en…...
深入Django(三)
Django视图(Views)详解 引言 在前两天的博客中,我们介绍了Django的基本概念和模型系统。今天,我们将深入探讨Django的视图(Views),它们是处理用户请求和返回响应的地方。 什么是Django视图&a…...
观测云赋能「阿里云飞天企业版」,打造全方位监控观测解决方案
近日,观测云成功通过了「阿里云飞天企业版」的生态集成认证测试,并荣获阿里云颁发的产品生态集成认证证书。作为监控观测领域的领军者,观测云一直专注于提供统一的数据视角,助力用户构建起全球范围内的端到端全链路可观测服务。此…...
51单片机第27步_单片机工作在睡眠模式
重点学习51单片机工作在睡眠模式。 1、进入“睡眠模式”的方法 通过将PCON寄存器中的PDWN置1,则CPU会进入“睡眠模式”。在“睡眠模式”中,晶振将停止工作,因此,定时器和串口都将停止工作,只有外部中断继续工作。如果单片机电源…...
互联网应用主流框架整合之SpringCloud微服务治理
微服务架构理念 关于微服务的概念、理念及设计相关内容,并没有特别严格的边界和定义,某种意义上说,适合的就是最好的,在之前的文章中有过详细的阐述,微服务[v1.0.0][Spring生态概述]、微服务[设计与运行]、微服务[v1.…...
19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...
macOS多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用
文章目录 问题现象问题原因解决办法 问题现象 macOS启动台(Launchpad)多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用。 问题原因 很明显,都是Google家的办公全家桶。这些应用并不是通过独立安装的…...
Matlab | matlab常用命令总结
常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...
如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件,用于在原生应用中加载 HTML 页面: 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...
服务器--宝塔命令
一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行! sudo su - 1. CentOS 系统: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...
JVM虚拟机:内存结构、垃圾回收、性能优化
1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...
七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...
【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)
前言: 双亲委派机制对于面试这块来说非常重要,在实际开发中也是经常遇见需要打破双亲委派的需求,今天我们一起来探索一下什么是双亲委派机制,在此之前我们先介绍一下类的加载器。 目录 编辑 前言: 类加载器 1. …...

