Go 项目依赖注入wire工具最佳实践介绍与使用
文章目录
- 一、引入
- 二、控制反转与依赖注入
- 三、为什么需要依赖注入工具
- 3.1 示例
- 3.2 依赖注入写法与非依赖注入写法
- 四、wire 工具介绍与安装
- 4.1 wire 基本介绍
- 4.2 安装
- 五、Wire 的基本使用
- 5.1 前置代码准备
- 5.2 使用 Wire 工具生成代码
- 六、Wire 核心技术
- 5.1 抽象语法树分析
- 5.2 模板编程
- 七、Wire 的核心概念
- 7.1 两个核心概念
- 7.2 Wire 提供者(providers)
- 7.3 Wire 注入器(injectors)
- 八、Wire 的高级用法
- 8.1 绑定接口
- 8.2 结构体提供者(Struct Providers)
- 8.3 绑定值
- 8.4 使用结构体字段作为提供者(providers)
- 8.5 清理函数
- 8.6 备用注入器语法
- 九、参考文档
一、引入
在Go语言的项目开发中,为了提高代码的可测试性和可维护性,我们通常会采用依赖注入(Dependency Injection,简称DI)的设计模式。依赖注入可以让高层模块不依赖底层模块的具体实现,而是通过抽象来互相依赖,从而使得模块之间的耦合度降低,系统的灵活性和可扩展性增强。
二、控制反转与依赖注入
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI)。依赖注入是生成灵活和松散耦合代码的标准技术,通过明确地向组件提供它们所需要的所有依赖关系。在 Go 中通常采用将依赖项作为参数传递给构造函数的形式:
构造函数NewUserRepository在创建UserRepository时需要从外部将依赖项db作为参数传入,我们在UserRepository中无需关注db的创建逻辑,实现了代码解耦。
// NewUserRepository 创建BookRepo的构造函数
func NewUserRepository(db *gorm.DB) *UserRepository {return &UserRepository{db: db}
}
区别于控制反转,如果在NewUserRepository函数中自行创建相关依赖,这将导致代码高度耦合并且难以维护和调试。
// NewUserRepository 创建UserRepository的构造函数
func NewUserRepository() *UserRepository {db, _ := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})return &UserRepository{db: db}
}
三、为什么需要依赖注入工具
3.1 示例
如果上面示例代码不够清晰的话,我们来看这两段代码:
// NewUserRepositoryV1非依赖注入的写法
func NewUserRepositoryV1(dbCfg DBConfig, c CacheConfig)*UserRepository{db, err := gorm.Open(mysql.Open(dbcfg.DSN))if err != nil {panic(err)}ud = dao.NewUserDAO(db)uc = cache.NewUserCache(redis.NewClient(&redis.Options{Addr: c.Addr,}))return &UserRepository{dao: ud,cache: uc,}
}// NewUserRepository 依赖注入的写法
func NewUserRepository(d *dao.UserDAO, c *cache.UserCache)*UserRepository{return &UserRepository{dao: d,cache: c,}
}
可以清楚地看到,这两段代码展示了在Go语言中实现依赖注入的两种不同方式。
第一段代码 NewUserRepositoryV1 是非依赖注入的写法。在这个函数中,UserRepository 的依赖(db 和 cache)是在函数内部创建的。这种方式的问题在于,它违反了单一职责原则,因为 NewUserRepositoryV1 不仅负责创建 UserRepository 实例,还负责创建其依赖的数据库和缓存客户端。这样做会导致代码耦合度较高,难以测试和维护。
第二段代码 NewUserRepository 是依赖注入的写法。这个函数接受 UserRepository 的依赖(*dao.UserDAO 和 *cache.UserCache)作为参数,而不是在函数内部创建它们。这种方式使得 UserRepository 的创建与它的依赖解耦,更容易测试,因为你可以轻松地为 UserRepository 提供模拟的依赖项。此外,这种写法也更符合依赖注入的原则,因为它将控制反转给了调用者,由调用者来决定 UserRepository 实例化时使用哪些依赖项。
3.2 依赖注入写法与非依赖注入写法
依赖注入写法:不关心依赖是如何构造的。
非依赖注入写法:必须自己初始化依赖,比如说 Repository 需要知道如何初始化 DAO 和 Cache。由此带来的缺点是:
- 深度耦合依赖的初始化过程。
- 往往需要定义额外的
Config类型来传递依赖所需的配置信息。 - 一旦依赖增加新的配置,或者更改了初始化过程,都要跟着修改。
- 缺乏扩展性。
- 测试不友好。
- 难以复用公共组件,例如 DB 或 Redis 之类的客户端。
四、wire 工具介绍与安装
4.1 wire 基本介绍
-
Wire 是一个的 Google 开源专为依赖注入(
Dependency Injection)设计的代码生成工具,通过自动生成代码的方式在初始编译过程中完成依赖注入。它可以自动生成用于化各种依赖关系的代码,从而帮助我们更轻松地管理和注入依赖关系。 -
Wire分成两部分,一个是在项目中使用的依赖, 一个是命令行工具。
4.2 安装
go install github.com/google/wire/cmd/wire@latest
五、Wire 的基本使用
5.1 前置代码准备
目录结构如下:
wire
├── db.go # 数据库相关代码
├── go.mod # Go模块依赖配置文件
├── go.sum # Go模块依赖校验文件
├── main.go # 程序入口文件
├── repository # 存放数据访问层代码的目录
│ ├── dao # 数据访问对象(DAO)目录
│ │ └── user.go # 用户相关的DAO实现
│ └── user.go # 用户仓库实现
├── wire.go # Wire依赖注入配置文件
repository/dao/user.go文件:
// repository/dao/user.go
package daoimport "gorm.io/gorm"type UserDAO struct {db *gorm.DB
}func NewUserDAO(db *gorm.DB) *UserDAO {return &UserDAO{db: db,}
}
repository/user.go 文件:
// repository/user.go
package repositoryimport "wire/repository/dao"type UserRepository struct {dao *dao.UserDAO
}func NewUserRepository(dao *dao.UserDAO) *UserRepository {return &UserRepository{dao: dao,}
}
db.go 文件:
// db.go
package wireimport ("gorm.io/driver/mysql""gorm.io/gorm"
)func InitDB() *gorm.DB {db, err := gorm.Open(mysql.Open("dsn"))if err != nil {panic(err)}return db
}
main.go 文件:
package wireimport ("fmt""gorm.io/driver/mysql""gorm.io/gorm""wire/repository""wire/repository/dao"
)func main() {// 非依赖注入db, err := gorm.Open(mysql.Open("dsn"))if err != nil {panic(err)}ud := dao.NewUserDAO(db)repo := repository.NewUserRepository(ud)fmt.Println(repo)
}
5.2 使用 Wire 工具生成代码
现在我们已经有了基本的代码结构,接下来我们将使用 wire 工具来生成依赖注入的代码。
首先,确保你已经安装了 wire 工具。如果没有安装,可以使用以下命令安装:
go get github.com/google/wire/cmd/wire
接下来,我们需要创建一个 wire 的配置文件,通常命名为 wire.go。在这个文件中,我们将使用 wire 的语法来指定如何构建 UserRepository 实例。
wire.go 文件:
//go:build wireinject// 让 wire 来注入这里的代码
package wireimport ("github.com/google/wire""wire/repository""wire/repository/dao"
)func InitRepository() *repository.UserRepository {// 我只在这里声明我要用的各种东西,但是具体怎么构造,怎么编排顺序// 这个方法里面传入各个组件的初始化方法wire.Build(InitDB, repository.NewUserRepository, dao.NewUserDAO)return new(repository.UserRepository)
}
这段代码是使用 wire 工具进行依赖注入的配置文件。在这个文件中,我们定义了一个函数 InitRepository,这个函数的目的是为了生成一个 *repository.UserRepository 的实例。但是,这个函数本身并不包含具体的实现代码,而是依赖于 wire 工具来注入依赖。
让我们逐步解释这段代码:
-
构建约束指令:
//go:build wireinject这行注释是一个构建约束,它告诉
go build只有在满足条件wireinject的情况下才应该构建这个文件。wireinject是一个特殊的标签,用于指示wire工具处理这个文件。 -
导入包:
import ("github.com/google/wire""wire/repository""wire/repository/dao" )这部分导入了必要的包,包括
wire工具库,以及项目中的repository和dao包,这些包包含了我们需要注入的依赖。 -
InitRepository 函数:
func InitRepository() *repository.UserRepository {// 我只在这里声明我要用的各种东西,但是具体怎么构造,怎么编排顺序// 这个方法里面传入各个组件的初始化方法wire.Build(InitDB, repository.NewUserRepository, dao.NewUserDAO)return new(repository.UserRepository) }这个函数是
wire注入的目标。它声明了一个返回*repository.UserRepository的函数,但是函数体内部没有具体的实现代码。wire.Build函数调用是关键, 主要是连接或绑定我们之前定义的所有初始化函数。当我们运行wire工具来生成代码时,它就会根据这些依赖关系来自动创建和注入所需的实例。,这些函数按照依赖关系被调用,以正确地构造和注入UserRepository实例所需的依赖。InitDB是初始化数据库连接的函数。repository.NewUserRepository是创建UserRepository实例的函数。dao.NewUserDAO是创建UserDAO实例的函数。
wire工具会自动生成这些函数调用的代码,并确保依赖关系得到满足。
-
返回语句:
return new(repository.UserRepository)这个返回语句是必须的,尽管它实际上并不会被执行。
wire工具会生成一个替换这个函数体的代码,其中包括所有必要的依赖注入逻辑。
在编写完wire.go文件后,你需要运行wire命令来生成实际的依赖注入代码。生成的代码将被放在一个名为wire_gen.go的文件中,这个文件应该被提交到你的版本控制系统中。
现在,我们可以运行 wire 命令来生成依赖注入的代码:
wire
这个命令会扫描 wire.go 文件,并生成一个新的 Go 文件 wire_gen.go,其中包含了 InitializeUserRepository 函数的实现,这个函数会创建并返回一个 UserRepository 实例,其依赖项已经自动注入。

生成 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 wireimport ("wire/repository""wire/repository/dao"
)// Injectors from wire.go:func InitRepository() *repository.UserRepository {db := InitDB()userDAO := dao.NewUserDAO(db)userRepository := repository.NewUserRepository(userDAO)return userRepository
}
最后,我们需要修改 main.go 文件,使用 wire 生成的代码来获取 UserRepository 实例:
package wirefunc main() {InitRepository()
}
现在,当我们运行 main.go 时,它将使用 wire 工具生成的代码来初始化 UserRepository,包括其依赖的 UserDAO 和数据库连接。这样,我们就实现了依赖注入,并且代码更加简洁、易于维护。
六、Wire 核心技术
5.1 抽象语法树分析
wire 工具的工作原理是基于对Go代码的抽象语法树(Abstract Syntax Tree,简称AST)的分析。AST是源代码的抽象语法结构的树状表示,它以树的形式表现编程语言的语法结构。wire 工具通过分析AST来理解代码中的依赖关系。
在Go中,go/ast 包提供了解析Go源文件并构建AST的功能。wire 工具利用这个包来遍历和分析项目的Go代码,识别出所有的依赖项,并构建出依赖关系图。这个依赖关系图随后被用来生成注入依赖的代码。
5.2 模板编程
wire 工具生成代码的过程也涉及到模板编程。模板编程是一种编程范式,它允许开发者定义一个模板,然后使用具体的数据来填充这个模板,生成最终的代码或文本。
在wire中,虽然不直接使用Go语言的模板引擎(如text/template或html/template),但它的工作原理与模板编程类似。wire定义了一套自己的语法来描述依赖关系,然后根据这些描述生成具体的Go代码。
wire的语法主要包括以下几个部分:
wire.NewSet:定义一组相关的依赖,通常包括一个或多个构造函数。wire.Build:指定生成代码时应该使用哪些依赖集合。bind函数:用于绑定接口和实现,告诉wire如何创建接口的实例。
wire工具通过这些语法来构建一个依赖图,然后根据这个图生成一个函数,该函数负责创建并返回所有必要的组件实例,同时处理它们之间的依赖关系。
通过结合抽象语法树分析和模板编程,wire工具能够提供一种声明式的依赖注入方法,让开发者能够专注于定义依赖关系,而不是手动编写依赖注入的代码。这不仅减少了重复劳动,还提高了代码的可维护性和降低了出错的可能性。
七、Wire 的核心概念
7.1 两个核心概念
在 wire 中,有两个核心概念:提供者(providers)和注入器(injectors)。
7.2 Wire 提供者(providers)
提供者 是一个普通有返回值的 Go 函数,它负责创建一个对象或者提供依赖。在 wire 的上下文中,提供者可以是任何返回一个或多个值的函数。这些返回值将成为注入器函数的参数。提供者函数通常负责初始化组件,比如数据库连接、服务实例等。并且提供者的返回值不仅限于一个,如果有需要的话,可以额外添加一个 error 的返回值。
例如,一个提供者函数可能会创建并返回一个数据库连接:
func NewDBConnection(dsn string) (*gorm.DB, error) {db, err := gorm.Open(mysql.Open(dsn))if err != nil {return nil, err}return db, nil
}
提供者函数可以分组为提供者函数集(provider set)。使用wire.NewSet 函数可以将多个提供者函数添加到一个集合中。举个例子,例如将 user 相关的 handler 和 service 进行组合:
package webvar UserSet = wire.NewSet(NewUserHandler, service.NewUserService)
使用 wire.NewSet 函数将提供者进行分组,该函数返回一个 ProviderSet 结构体。不仅如此,wire.NewSet 还能对多个 ProviderSet 进行分组 wire.NewSet(UserSet, XxxSet) 。
package demoimport (// ..."example.com/some/other/pkg"
)// ...var MegaSet = wire.NewSet(UserSet, pkg.OtherSet)
7.3 Wire 注入器(injectors)
注入器(injectors)的作用是将所有的提供者(providers)连接起来,要声明一个注入器函数只需要在函数体中调用wire.Build()。这个函数的返回值也无关紧要,只要它们的类型正确即可。这些值在生成的代码中将被忽略。回顾一下我们之前的代码:
//go:build wireinject// 让 wire 来注入这里的代码
package wireimport ("github.com/google/wire""wire/repository""wire/repository/dao"
)func InitRepository() *repository.UserRepository {// 我只在这里声明我要用的各种东西,但是具体怎么构造,怎么编排顺序// 这个方法里面传入各个组件的初始化方法wire.Build(InitDB, repository.NewUserRepository, dao.NewUserDAO)return new(repository.UserRepository)
}
在这个例子中,InitRepository 是一个注入器,它依赖 InitDB 和 repository.NewUserRepository 这两个提供者。
与提供者一样,注入器也可以输入参数(然后将其发送给提供者),并且可以返回错误。wire.Build的参数和wire.NewSet一样:都是提供者集合。这些就在该注入器的代码生成期间使用的提供者集。
八、Wire 的高级用法
8.1 绑定接口
依赖项注入通常用于绑定接口的具体实现。wire通过类型标识将输入与输出匹配,因此倾向于创建一个返回接口类型的提供者。然而,这也不是习惯写法,因为Go的最佳实践是返回具体类型。你可以在提供者集中声明接口绑定.
我们对之前的代码进行改造:
首先,我们在UserRepository接口中定义一些方法。例如,我们可以定义一个GetUser方法,该方法接收一个用户ID,并返回相应的用户。 在repository/user.go文件中:
package repositoryimport ("wire/repository/dao""gorm.io/gorm"
)type UserRepository interface {GetUser(id uint) (*User, error)
}type UserRepositoryImpl struct {dao *dao.UserDAO
}func (r *UserRepositoryImpl) GetUser(id uint) (*User, error) {return r.dao.GetUser(id)
}func NewUserRepository(dao *dao.UserDAO) UserRepository {return &UserRepositoryImpl{dao: dao,}
}
然后,我们在UserDAO中实现这个GetUser方法。在repository/dao/user.go文件中:
package daoimport ("gorm.io/gorm"
)type User struct {ID uint// other fields...
}type UserDAO struct {db *gorm.DB
}func (dao *UserDAO) GetUser(id uint) (*User, error) {var user Userresult := dao.db.First(&user, id)if result.Error != nil {return nil, result.Error}return &user, nil
}func NewUserDAO(db *gorm.DB) *UserDAO {return &UserDAO{db: db,}
}
最后,我们需要更新wire.go文件中的InitRepository函数,以返回UserRepository接口,而不是具体的实现。 在wire.go文件中:
//go:build wireinjectpackage wireimport ("github.com/google/wire""wire/repository""wire/repository/dao"
)func InitRepository() repository.UserRepository {wire.Build(InitDB, repository.NewUserRepository, dao.NewUserDAO)return &repository.UserRepositoryImpl{}
}
使用 wire.Bind 来建立接口类型和具体的实现类型之间的绑定关系,这样 Wire 工具就可以根据这个绑定关系进行类型匹配并生成代码。
wire.Bind 函数的第一个参数是指向所需接口类型值的指针,第二个实参是指向实现该接口的类型值的指针。
8.2 结构体提供者(Struct Providers)
Wire 库有一个函数是 wire.Struct,它能根据现有的类型进行构造结构体,我们来看看下面的例子:
package mainimport "github.com/google/wire"type Name stringfunc NewName() Name {return "小米SU7"
}type PublicAccount stringfunc NewPublicAccount() PublicAccount {return "新一代车神"
}type User struct {MyName NameMyPublicAccount PublicAccount
}func InitializeUser() *User {wire.Build(NewName,NewPublicAccount,wire.Struct(new(User), "MyName", "MyPublicAccount"),)return &User{}
}
上述代码中,首先定义了自定义类型 Name 和 PublicAccount 以及结构体类型 User,并分别提供了 Name 和 PublicAccount 的初始化函数(providers)。然后定义一个注入器(injectors)InitializeUser,用于构造连接提供者并构造 *User 实例。
使用 wire.Struct 函数需要传递两个参数,第一个参数是结构体类型的指针值,另一个参数是一个可变参数,表示需要注入的结构体字段的名称集。
根据上述代码,使用 Wire 工具生成的代码如下所示:
func InitializeUser() *User {name := NewName()publicAccount := NewPublicAccount()user := &User{MyName: name,MyPublicAccount: publicAccount,}return user
}
如果我们不想返回指针类型,只需要修改 InitializeUser 函数的返回值为非指针即可。
8.3 绑定值
有时,将基本值(通常为nil)绑定到类型是有用的。你可以向提供程序集添加一个值表达式,而不是让注入器依赖于一次性函数提供者(providers)。
func InjectUser() User {wire.Build(wire.Value(User{MyName: "小米SU7"}))return User{}
}
在上述代码中,使用 wire.Value 函数通过表达式直接指定 MyName 的值,生成的代码如下所示:
func InjectUser() User {user := _wireUserValuereturn user
}var (_wireUserValue = User{MyName: "小米SU7"}
)
需要注意的是,值表达式将被复制到生成的代码文件中。
对于接口类型,可以使用 InterfaceValue:
func InjectPostService() service.IPostService {wire.Build(wire.InterfaceValue(new(service.IPostService), &service.PostService{}))return nil
}
8.4 使用结构体字段作为提供者(providers)
有些时候,你可以使用结构体的某个字段作为提供者,从而生成一个类似 GetXXX 的函数。
func GetUserName() Name {wire.Build(NewUser,wire.FieldsOf(new(User), "MyName"),)return ""
}
你可以使用 wire.FieldsOf 函数添加任意字段,生成的代码如下所示:
func GetUserName() Name {user := NewUser()name := user.MyNamereturn name
}func NewUser() User {return User{MyName: Name("小米SU7"), MyPublicAccount: PublicAccount("新一代车神!")}
}
8.5 清理函数
如果一个提供者创建了一个需要清理的值(例如关闭一个文件),那么它可以返回一个闭包来清理资源。注入器会用它来给调用者返回一个聚合的清理函数,或者在注入器实现中稍后调用的提供商返回错误时清理资源。
func provideFile(log Logger, path Path) (*os.File, func(), error) {f, err := os.Open(string(path))if err != nil {return nil, nil, err}cleanup := func() {if err := f.Close(); err != nil {log.Log(err)}}return f, cleanup, nil
}
8.6 备用注入器语法
如果你不喜欢在注入器函数声明的末尾编写类似return Foo{}, nil的语句,那么你可以简单粗暴地使用panic:
func InitializeGin() *gin.Engine {panic(wire.Build(/* ... */))
}
九、参考文档
- 掘金依赖注入工具-wire
- 李文周的博客-依赖注入工具-wire
相关文章:
Go 项目依赖注入wire工具最佳实践介绍与使用
文章目录 一、引入二、控制反转与依赖注入三、为什么需要依赖注入工具3.1 示例3.2 依赖注入写法与非依赖注入写法 四、wire 工具介绍与安装4.1 wire 基本介绍4.2 安装 五、Wire 的基本使用5.1 前置代码准备5.2 使用 Wire 工具生成代码 六、Wire 核心技术5.1 抽象语法树分析5.2 …...
地推网推拉新致富是真的吗?靠谱平台揭秘
在互联网时代,各种平台层出不穷。为了吸引更多用户,这些平台常常会推出各种地推网推拉新活动。如果你懂得如何利用,那么你也有机会从中获得一笔不小的收入。 当然,在地推网推拉新赚钱的过程中,也需要注意一些问题。首…...
VTK使用交互器来从三维体数据中提取二维切片
VTK中鼠标消息是在交互类型对象(interactorstyle)中响应,因此通过为交互类型对象(interactorstyle)添加观察者(observer)来监听相应的消息,当消息触发时,由命令模式执行相…...
NCBI 数据下载
网上介绍的那几种直接下载NCBI数据的方法大都下载速度很慢,但是EBI (European Bioinformatics Institute) 下载很快,而且它的数据库和NCBI是共享的,所以我们可以直接从 EBI 下载。 1 、 确定要下载的 SRA 编号; 2 、 EBI (https…...
【Rust】基础语法
变量,基本类型,函数,注释和控制流,这些几乎是每种编程语言都具有的编程概念。 这些基础概念将存在于每个 Rust 程序中,及早学习它们将使你以最快的速度学习 Rust 的使用。 变量 首先必须说明,Rust 是强类…...
JVM基础:类的生命周期详解
JDK版本:jdk8 IDEA版本:IntelliJ IDEA 2022.1.3 文章目录 一. 生命周期概述二. 加载阶段(Loading)2.1 加载步骤2.2 查看内存中的对象 三. 连接阶段(Linking)3.1 连接之验证3.2 连接之准备3.3 连接阶段之解析 四. 初始化阶段(Initialization)4.1 单个类的…...
【Canvas技法】在Canvas按圆周绘制图形或是标注文字时,角度累加的方向为顺时针,起点为x轴正向
【图解说明】 【核心代码】 // 画圆弧及方向for(var i0;i<4;i){var startMath.PI/2*i;var endstartMath.PI/2;var x1180*Math.cos(start);var y1180*Math.sin(start);var x2180*Math.cos(end);var y2180*Math.sin(end);ctx.beginPath();ctx.arc(0,0,180,start,end,false);ct…...
计算机网络-TCP断开连接阶段错误应对机制
连接断开阶段 四次挥手机制:TCP连接的断开需要四次挥手,这是因为双方都需要独立地关闭数据传输。第二次和第三次挥手不能合并,因为在回复第二次挥手的时候,可能还有数据没有接收完成,所以需要先回复ACK报文,…...
springboot动态使用DruidDataSource切换数据源(动态配置多个数据源)
1、添加依赖,在pom文件中添加 <dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>2.5.0</version></dependency><dependency><grou…...
P8786 [蓝桥杯 2022 省 B] 李白打酒加强版
【dfs题解】---只有50分 (头一回自己用dfs做出来了dp的hard等级的大题,从来没有拿50分这么高兴过哈哈哈哈哈) #include <bits/stdc.h> using namespace std; int n,m; long long ans0; const long long mol1e97; void dfs(int h,int d,int sum) {if(h<0|…...
没有网没有移动存储的情况下两台电脑如何互相传输数据
无网无移动存储情况下两台电脑数据互传探秘 一、直连网线传输数据二、局域网文件共享其他 在信息化时代的今天,电脑作为重要的数据处理工具,在日常生活和工作中扮演着不可或缺的角色。然而,有时我们会遇到一些特殊情况,如没有网络…...
如何用putty通过ssh连接ubuntu
1. 下载和安装PuTTY 访问PuTTY官网下载PuTTY的最新版本。 2. 打开PuTTY 解压下载的文件后,找到PuTTY文件并双击打开。 3. 配置SSH连接 在ubuntu下安装ssh服务在安装ssh时,我一直遇到一个问题,原因是我的虚拟机连不上网,反复实…...
java如何实现rabbitmq的消息确认机制和消息持久化机制配置和示例
在Java中,使用RabbitMQ的客户端库(通常是AMQP客户端库,如RabbitMQ的Java客户端)可以方便地实现消息确认机制和消息持久化机制。以下是如何实现这两个机制的示例。 1、消息确认机制 RabbitMQ支持两种类型的确认:生产者…...
react 组件:Suspense
允许在子组件完成加载前展示后备方案。 children:真正的 UI 渲染内容。如果 children 在渲染中被挂起,Suspense 边界将会渲染 fallback。 fallback:真正的 UI 未渲染完成时代替其渲染的备用 UI,它可以是任何有效的 React 节点。后…...
2024-4-5修改vscode的代理
今天在vs code 上面配置go环境的时候出现了以下的报错: 2024-04-05 16:18:00.786 [info] Installing golang.org/x/tools/goplslatest FAILED 2024-04-05 16:18:00.786 [info] { “code”: 1, “killed”: false, “signal”: null, “cmd”: “E:\Go\bin\go.exe in…...
python字符切片的规则
跟range一样有三个参数,分别是x:y:z,代表的含义分别为左边界,右边界(注意该范围是左闭右开的,也就是说取不到右值)和步长。 1. 切片是从左往右还是从右往左,看的是步长的正负,如果步…...
C++ 的内存安全与效率
在C编程中,内存安全和效率是两个至关重要的考虑因素。 内存安全涉及确保程序在分配和使用内存时不会发生错误,如内存泄漏、悬挂指针、越界访问、空指针解引用等; 效率则关注如何有效地使用内存资源,减少不必要的内存分配和释放操…...
Go 实战|使用 Wails 构建轻量级的桌面应用:仿微信登录界面 Demo
概述 本文探讨 Wails 框架的使用,从搭建环境到开发,再到最终的构建打包,本项目源码 GitHub 地址:https://github.com/mazeyqian/go-run-wechat-demo 前言 Wails 是一个跨平台桌面应用开发框架,他允许开发者利用 Go …...
c++取经之路(其五)——类和对象拷贝构造函数
概念:拷贝构造函数,只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。 特征: 1. 拷贝构造函数是构造函数的一个重载形式 如: 2. 拷贝…...
YOLOv8最新改进系列:融合最新顶会提出的HCANet网络中卷积和注意力融合模块(CAFM),有效提升小目标检测性能,大幅度拉升目标检测效果!遥遥领先!
YOLOv8最新改进系列:YOLOv8最新改进系列:融合最新顶会提出的HCANet网络中卷积和注意力融合模块(CAFM),有效提升小目标检测性能,大幅度拉升目标检测效果!遥遥领先! B站全文戳这里! 详细的改进教程以及源码…...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...
VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...
DAY 47
三、通道注意力 3.1 通道注意力的定义 # 新增:通道注意力模块(SE模块) class ChannelAttention(nn.Module):"""通道注意力模块(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_rat…...
Spring AI与Spring Modulith核心技术解析
Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...
Ubuntu Cursor升级成v1.0
0. 当前版本低 使用当前 Cursor v0.50时 GitHub Copilot Chat 打不开,快捷键也不好用,当看到 Cursor 升级后,还是蛮高兴的 1. 下载 Cursor 下载地址:https://www.cursor.com/cn/downloads 点击下载 Linux (x64) ,…...
【前端异常】JavaScript错误处理:分析 Uncaught (in promise) error
在前端开发中,JavaScript 异常是不可避免的。随着现代前端应用越来越多地使用异步操作(如 Promise、async/await 等),开发者常常会遇到 Uncaught (in promise) error 错误。这个错误是由于未正确处理 Promise 的拒绝(r…...
前端中slice和splic的区别
1. slice slice 用于从数组中提取一部分元素,返回一个新的数组。 特点: 不修改原数组:slice 不会改变原数组,而是返回一个新的数组。提取数组的部分:slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...
LCTF液晶可调谐滤波器在多光谱相机捕捉无人机目标检测中的作用
中达瑞和自2005年成立以来,一直在光谱成像领域深度钻研和发展,始终致力于研发高性能、高可靠性的光谱成像相机,为科研院校提供更优的产品和服务。在《低空背景下无人机目标的光谱特征研究及目标检测应用》这篇论文中提到中达瑞和 LCTF 作为多…...
