Golang?依賴注入經(jīng)典解決方案uber/fx理論解析
開篇
今天繼續(xù)我們的【依賴注入開源解決方案系列】, Dependency Injection 業(yè)界的開源庫(kù)非常多,大家可以憑借自己的喜好也業(yè)務(wù)的復(fù)雜度來(lái)選型。基于 github star 數(shù)量以及方案的全面性,易用性上。推薦這兩個(gè):
1.【代碼生成】派系推薦大家用 wire, 做的事情非常輕量級(jí),省下大家手寫代碼的負(fù)擔(dān),沒有太多 DI 工具帶來(lái)的結(jié)構(gòu)性改造;
2.【反射】派系推薦大家用 uber/fx,功能非常強(qiáng)大,很全面,也比較符合直覺。
二者都需要顯式聲明依賴,這一點(diǎn)對(duì)程序的可讀性是好事,兩個(gè)庫(kù)的 star 也都非常多。建議大家有興趣的話研讀一下。不管是 codegen 還是 reflect(結(jié)合 interface{},泛型)都是 Golang 學(xué)習(xí)體系中必須的能力,否則很難實(shí)現(xiàn)通用的一些能力。
今天我們來(lái)看看 uber/fx 這個(gè)反射派系的經(jīng)典之作,這是 uber 家基于 dig 的又一步進(jìn)化。
uber/fx
Fx is a dependency injection system for Go.
fx 是 uber 2017 年開源的依賴注入解決方案,不僅僅支持常規(guī)的依賴注入,還支持生命周期管理。
從官方的視角看,fx 能為開發(fā)者提供的三大優(yōu)勢(shì):
代碼復(fù)用:方便開發(fā)者構(gòu)建松耦合,可復(fù)用的組件;
消除全局狀態(tài):Fx 會(huì)幫我們維護(hù)好單例,無(wú)需借用 init() 函數(shù)或者全局變量來(lái)做這件事了;
經(jīng)過(guò)多年 Uber 內(nèi)部驗(yàn)證,足夠可信。
我們從 uber-go/fx 看到的是 v1 的版本,fx 是遵循 SemVer 規(guī)范的,保障了兼容性,這一點(diǎn)大家可以放心。
從劣勢(shì)的角度分析,其實(shí) uber/fx 最大的劣勢(shì)還是大量使用反射,導(dǎo)致項(xiàng)目啟動(dòng)階段會(huì)需要一些性能消耗,但這一般是可以接受的。如果對(duì)性能有高要求,建議還是采取 wire 這類 codegen 的依賴注入解法。
目前市面上對(duì) Fx 的介紹文章并不多,筆者在學(xué)習(xí)的時(shí)候也啃了很長(zhǎng)時(shí)間官方文檔,這一點(diǎn)有好有壞。的確,再多的例子,再多的介紹,也不如一份完善的官方文檔更有力。但同時(shí)也給初學(xué)者帶來(lái)較高的門檻。
今天這篇文章希望從一個(gè)開發(fā)者的角度,帶大家理解 Fx 如何使用。
添加 fx 的依賴需要用下面的命令:
go get go.uber.org/fx@v1
后面我們會(huì)有專門的一篇文章,拿一個(gè)實(shí)戰(zhàn)項(xiàng)目來(lái)給大家展示,如何使用 Fx,大家同時(shí)也可以參考官方 README 中的 Getting Started 來(lái)熟悉。
下面一步一步來(lái),我們先來(lái)看看 uber/fx 中的核心概念。
provider 聲明依賴關(guān)系
在我們的業(yè)務(wù)服務(wù)的聲明周期中,對(duì)于各個(gè) module 的初始化應(yīng)該基于我們的 dependency graph 來(lái)合理進(jìn)行。先初始化無(wú)外部依賴的對(duì)象,隨后基于這些對(duì)象,初始化對(duì)它們有依賴的對(duì)象。
Provider 就是我們常說(shuō)的構(gòu)造器,能夠提供對(duì)象的生成邏輯。在 Fx 啟動(dòng)時(shí)會(huì)創(chuàng)建一個(gè)容器,我們需要將業(yè)務(wù)的構(gòu)造器傳進(jìn)來(lái),作為 Provider。類似下面這樣:
app = fx.New( fx.Provide(newZapLogger), fx.Provide(newRedisClient), fx.Provide(newMeaningOfLifeCacheRedis), fx.Provide(newMeaningOfLifeHandler), )
這里面的 newXXX 函數(shù),就是我們的構(gòu)造器,類似這樣:
func NewLogger() *log.Logger { logger := log.New(os.Stdout, "" /* prefix */, 0 /* flags */) logger.Print("Executing NewLogger.") return logger }
我們只需要通過(guò) fx.Provide 方法傳入進(jìn)容器,就完成了將對(duì)象提供出去的使命。隨后 fx 會(huì)在需要的時(shí)候調(diào)用我們的 Provider,生成單例對(duì)象使用。
當(dāng)然,構(gòu)造器不光是這種沒有入?yún)⒌?。還有一些對(duì)象是需要顯式的傳入依賴:
func NewHandler(logger *log.Logger) (http.Handler, error) { logger.Print("Executing NewHandler.") return http.HandlerFunc(func(http.ResponseWriter, *http.Request) { logger.Print("Got a request.") }), nil }
注意,這里返回的 http.Handler 也可以成為別人的依賴。這些,我們通通不用關(guān)心!
fx 會(huì)自己通過(guò)反射,搞明白哪個(gè) Provider 需要什么,能提供什么。構(gòu)建出來(lái)整個(gè) dependency graph。
// Provide registers any number of constructor functions, teaching the // application how to instantiate various types. The supplied constructor // function(s) may depend on other types available in the application, must // return one or more objects, and may return an error. For example: // // // Constructs type *C, depends on *A and *B. // func(*A, *B) *C // // // Constructs type *C, depends on *A and *B, and indicates failure by // // returning an error. // func(*A, *B) (*C, error) // // // Constructs types *B and *C, depends on *A, and can fail. // func(*A) (*B, *C, error) // // The order in which constructors are provided doesn't matter, and passing // multiple Provide options appends to the application's collection of // constructors. Constructors are called only if one or more of their returned // types are needed, and their results are cached for reuse (so instances of a // type are effectively singletons within an application). Taken together, // these properties make it perfectly reasonable to Provide a large number of // constructors even if only a fraction of them are used. // // See the documentation of the In and Out types for advanced features, // including optional parameters and named instances. // // Constructor functions should perform as little external interaction as // possible, and should avoid spawning goroutines. Things like server listen // loops, background timer loops, and background processing goroutines should // instead be managed using Lifecycle callbacks. func Provide(constructors ...interface{}) Option { return provideOption{ Targets: constructors, Stack: fxreflect.CallerStack(1, 0), } }
作為開發(fā)者,我們只需要保證,所有我們需要的依賴,都通過(guò) fx.Provide 函數(shù)提供即可。另外需要注意,雖然上面我們是每個(gè) fx.Provide,都只包含一個(gè)構(gòu)造器,實(shí)際上他是支持多個(gè)構(gòu)造器的。
module 模塊化組織依賴
// Module is a named group of zero or more fx.Options. // A Module creates a scope in which certain operations are taken // place. For more information, see [Decorate], [Replace], or [Invoke]. func Module(name string, opts ...Option) Option { mo := moduleOption{ name: name, options: opts, } return mo }
fx 中的 module 也是經(jīng)典的概念。實(shí)際上我們?cè)谶M(jìn)行軟件開發(fā)時(shí),分層分包是不可避免的。而 fx 也是基于模塊化編程。使用 module 能夠幫助我們更方便的管理依賴:
/ ProvideLogger to fx func ProvideLogger() *zap.SugaredLogger { logger, _ := zap.NewProduction() slogger := logger.Sugar() return slogger } // Module provided to fx var Module = fx.Options( fx.Provide(ProvideLogger), )
我們的 Module 是一個(gè)可導(dǎo)出的變量,包含了一組 fx.Option,這里包含了各個(gè) Provider。
這樣,我們就不必要在容器初始化時(shí)傳入那么多 Provider 了,而是每個(gè) Module 干好自己的事即可。
func main() { fx.New( fx.Provide(http.NewServeMux), fx.Invoke(server.New), fx.Invoke(registerHooks), loggerfx.Module, ).Run() }
lifecycle 給應(yīng)用生命周期加上鉤子
// Lifecycle allows constructors to register callbacks that are executed on // application start and stop. See the documentation for App for details on Fx // applications' initialization, startup, and shutdown logic. type Lifecycle interface { Append(Hook) } // A Hook is a pair of start and stop callbacks, either of which can be nil. // If a Hook's OnStart callback isn't executed (because a previous OnStart // failure short-circuited application startup), its OnStop callback won't be // executed. type Hook struct { OnStart func(context.Context) error OnStop func(context.Context) error }
lifecycle 是 Fx 定義的一個(gè)接口。我們可以對(duì) fx.Lifecycle 進(jìn)行 append 操作,增加鉤子函數(shù),這里就可以支持我們訂閱一些指定行為,如 OnStart 和 OnStop。
如果執(zhí)行某個(gè) OnStart 鉤子時(shí)出現(xiàn)錯(cuò)誤,應(yīng)用會(huì)立刻停止后續(xù)的 OnStart,并針對(duì)此前已經(jīng)執(zhí)行過(guò) OnStart 的鉤子執(zhí)行對(duì)應(yīng)的 OnStop 用于清理資源。
這里 fx 加上了 15 秒的超時(shí)限制,通過(guò) context.Context 實(shí)現(xiàn),大家記得控制好自己的鉤子函數(shù)執(zhí)行時(shí)間。
invoker 應(yīng)用的啟動(dòng)器
provider 是懶加載的,僅僅 Provide 出來(lái)我們的構(gòu)造器,是不會(huì)當(dāng)時(shí)就觸發(fā)調(diào)用的,而 invoker 則能夠直接觸發(fā)業(yè)務(wù)提供的函數(shù)運(yùn)行。并且支持傳入一個(gè) fx.Lifecycle 作為入?yún)?,業(yè)務(wù)可以在這里 append 自己想要的 hook。
假設(shè)我們有一個(gè) http server,希望在 fx 應(yīng)用啟動(dòng)的時(shí)候同步開啟。這個(gè)時(shí)候就需要兩個(gè)入?yún)ⅲ?/p>
fx.Lifecycle
我們的主依賴(通常是對(duì)服務(wù)接口的實(shí)現(xiàn),一個(gè) handler)
我們將這里的邏輯封裝起來(lái),就可以作為一個(gè) invoker 讓 Fx 來(lái)調(diào)用了。看下示例代碼:
func runHttpServer(lifecycle fx.Lifecycle, molHandler *MeaningOfLifeHandler) { lifecycle.Append(fx.Hook{OnStart: func(context.Context) error { r := fasthttprouter.New() r.Handle(http.MethodGet, "/what-is-the-meaning-of-life", molHandler.Handle) return fasthttp.ListenAndServe("localhost:8080", r.Handler) }}) }
下面我們將它加入 Fx 容器初始化的流程中:
fx.New( fx.Provide(newZapLogger), fx.Provide(newRedisClient), fx.Provide(newMeaningOfLifeCacheRedis), fx.Provide(newMeaningOfLifeHandler), fx.Invoke(runHttpServer), )
這樣在創(chuàng)建容器時(shí),我們的 runHttpServer 就會(huì)被調(diào)用,進(jìn)而注冊(cè)了服務(wù)啟動(dòng)的邏輯。這里我們需要一個(gè) MeaningOfLifeHandler,F(xiàn)x 會(huì)觀察到這一點(diǎn),進(jìn)而到 Provider 里面挨個(gè)找依賴,每個(gè)類型對(duì)應(yīng)一個(gè)單例對(duì)象,通過(guò)懶加載的方式獲取到 MeaningOfLifeHandler 的所有依賴,以及子依賴。
其實(shí) Invoker 更多意義上看,像是一個(gè)觸發(fā)器。
我們可以有很多 Provider,但什么時(shí)候去調(diào)用這些函數(shù),生成依賴呢?Invoker 就是做這件事的。
// New creates and initializes an App, immediately executing any functions // registered via Invoke options. See the documentation of the App struct for // details on the application's initialization, startup, and shutdown logic. func New(opts ...Option) *App
最后,有了一個(gè)通過(guò) fx.New 生成的 fx 應(yīng)用,我們就可以通過(guò) Start 方法來(lái)啟動(dòng)了:
func main() { ? ?ctx, cancel := context.WithCancel(context.Background()) ? ?kill := make(chan os.Signal, 1) ? ?signal.Notify(kill) ? ?go func() { ? ? ? <-kill ? ? ? cancel() ? ?}() ? ?app := fx.New( ? ? ? fx.Provide(newZapLogger), ? ? ? fx.Provide(newRedisClient), ? ? ? fx.Provide(newMeaningOfLifeCacheRedis), ? ? ? fx.Provide(newMeaningOfLifeHandler), ? ? ? fx.Invoke(runHttpServer), ? ?) ? ?if err := app.Start(ctx);err != nil{ ? ? ? fmt.Println(err) ? ?} }
當(dāng)然,有了一個(gè) fx 應(yīng)用后,我們可以直接 fx.New().Run() 來(lái)啟動(dòng),也可以隨后通過(guò) app.Start(ctx) 方法啟動(dòng),配合 ctx 的取消和超時(shí)能力。二者皆可。
fx.In 封裝多個(gè)入?yún)?/h2>
當(dāng)構(gòu)造函數(shù)參數(shù)過(guò)多的時(shí)候,我們可以使用 fx.In 來(lái)統(tǒng)一注入,而不用在構(gòu)造器里一個(gè)個(gè)加參數(shù):
type ConstructorParam struct { ? ? fx.In ? ? Logger ?*log.Logger ? ? Handler http.Handler } type Object struct { ? ? Logger ?*log.Logger ? ? Handler http.Handler } func NewObject(p ConstructorParam) Object { ? ? return Object { ? ? ? ? Logger: ?p.Logger, ? ? ? ? Handler: p.Handler, ? ? } }
fx.Out 封裝多個(gè)出參
和 In 類似,有時(shí)候我們需要返回多個(gè)參數(shù),這時(shí)候一個(gè)個(gè)寫顯然比較笨重。我們可以用 fx.Out 的能力用結(jié)構(gòu)體來(lái)封裝:
type Result struct { ? ? fx.Out ? ? Logger ?*log.Logger ? ? Handler http.Handler } func NewResult() Result { ? ? // logger := xxx ? ? // handler := xxx ? ? return Result { ? ? ? ? Logger: ?logger, ? ? ? ? Handler: handler, ? ? } }
基于同類型提供多種實(shí)現(xiàn)
By default, Fx applications only allow one constructor for each type.
Fx 應(yīng)用默認(rèn)只允許每種類型存在一個(gè)構(gòu)造器,這種限制在一些時(shí)候是很痛的。
有些時(shí)候我們就是會(huì)針對(duì)一個(gè) interface 提供多種實(shí)現(xiàn),如果做不到,我們就只能在外面套一個(gè)類型,這和前一篇文章中我們提到的 wire 里的處理方式是一樣的:
type RedisA *redis.Client type RedisB *redis.Client
但這樣還是很笨重,有沒有比較優(yōu)雅的解決方案呢?
當(dāng)然有,要不 uber/fx 怎么能被稱為一個(gè)功能全面的 DI 方案呢?
既然是同類型,多個(gè)不同的值,我們可以給不同的實(shí)現(xiàn)命名來(lái)區(qū)分。進(jìn)而這涉及兩個(gè)部分:生產(chǎn)端 和 消費(fèi)端。
在提供依賴的時(shí)候,可以聲明它的名稱,進(jìn)而即便出現(xiàn)同類型的其他依賴,fx 也知道如何區(qū)分。
在獲取依賴的時(shí)候,也要指明我們需要的依賴的名稱具體是什么,而不只是簡(jiǎn)單的明確類型即可。
這里我們需要用到 fx.In 和 fx.Out 的能力。參照 官方文檔 我們來(lái)了解一下 fx 的解法:Named Values。
fx 支持開發(fā)者聲明 name 標(biāo)簽,用來(lái)給依賴「起名」,類似這樣:name:"rw"。
type GatewayParams struct { ? fx.In ? WriteToConn ?*sql.DB `name:"rw"` ? ReadFromConn *sql.DB `name:"ro" optional:"true"` } func NewCommentGateway(p GatewayParams, log *log.Logger) (*CommentGateway, error) { ? if p.ReadFromConn == nil { ? ? log.Print("Warning: Using RW connection for reads") ? ? p.ReadFromConn = p.WriteToConn ? } ? // ... } type ConnectionResult struct { ? fx.Out ? ReadWrite *sql.DB `name:"rw"` ? ReadOnly ?*sql.DB `name:"ro"` } func ConnectToDatabase(...) (ConnectionResult, error) { ? // ... ? return ConnectionResult{ReadWrite: rw, ReadOnly: ?ro}, nil }
這樣 fx 就知道,我們?nèi)?gòu)建 NewCommentGateway 的時(shí)候,傳入的 *sql.DB 需要是 rw 這個(gè)名稱的。而此前ConnectToDatabase 已經(jīng)提供了這個(gè)名稱,同類型的實(shí)例,所以依賴構(gòu)建成功。
使用起來(lái)非常簡(jiǎn)單,在我們對(duì) In 和 Out 的 wrapper 中聲明各個(gè)依賴的 name,也可以搭配 optional 標(biāo)簽使用。fx 支持任意多個(gè) name 的實(shí)例。
這里需要注意,同名稱的生產(chǎn)端和消費(fèi)端的類型必須一致,不能一個(gè)是 sql.DB 另一個(gè)是 *sql.DB。命名的能力只有在同類型的情況下才有用處。
Annotate 注解器
Annotate lets you annotate a function's parameters and returns without you having to declare separate struct definitions for them.
注解器能幫我們修改函數(shù)的入?yún)⒑统鰠?,無(wú)需定義單獨(dú)的結(jié)構(gòu)體。fx 的這個(gè)能力非常強(qiáng)大,目前暫時(shí)沒有看到其他 DI 工具能做到這一點(diǎn)。
func Annotate(t interface{}, anns ...Annotation) interface{} { result := annotated{Target: t} for _, ann := range anns { if err := ann.apply(&result); err != nil { return annotationError{ target: t, err: err, } } } return result }
我們來(lái)看看如何用 Annotate 來(lái)添加 ParamTag, ResultTag 來(lái)實(shí)現(xiàn)同一個(gè) interface 多種實(shí)現(xiàn)。
// Given, type Doer interface{ ... } // And three implementations, type GoodDoer struct{ ... } func NewGoodDoer() *GoodDoer type BadDoer struct{ ... } func NewBadDoer() *BadDoer type UglyDoer struct{ ... } func NewUglyDoer() *UglyDoer fx.Provide( ? fx.Annotate(NewGoodDoer, fx.As(new(Doer)), fx.ResultTags(`name:"good"`)), ? fx.Annotate(NewBadDoer, fx.As(new(Doer)), fx.ResultTags(`name:"bad"`)), ? fx.Annotate(NewUglyDoer, fx.As(new(Doer)), fx.ResultTags(`name:"ugly"`)), )
這里我們有 Doer 接口,以及對(duì)應(yīng)的三種實(shí)現(xiàn):GoodDoer, BadDoer, UglyDoer,三種實(shí)現(xiàn)的構(gòu)造器返回值甚至都不需要是Doer,完全可以是自己的 struct 類型。
這里還是不得不感慨 fx 強(qiáng)大的裝飾器能力。我們用一個(gè)簡(jiǎn)單的:
fx.Annotate(NewGoodDoer, fx.As(new(Doer)))
就可以對(duì)構(gòu)造器 NewGoodDoer 完成類型轉(zhuǎn)換。
這里還可以寫一個(gè) helper 函數(shù)簡(jiǎn)化一下處理:
func AsDoer(f any, name string) any { ? return fx.Anntoate(f, fx.As(new(Doer)), fx.ResultTags("name:" + strconv.Quote(name))) } fx.Provide( ?AsDoer(NewGoodDoer, "good"), ?AsDoer(NewBadDoer, "bad"), ?AsDoer(NewUglyDoer, "ugly"), )
與之相對(duì)的,提供依賴的時(shí)候我們用 ResultTag,消費(fèi)依賴的時(shí)候需要用 ParamTag。
func Do(good, bad, ugly Doer) { ? // ... } fx.Invoke( ? fx.Annotate(Do, fx.ParamTags(`name:"good"`, `name:"bad"`, `name:"ugly"`)), ) 這樣就無(wú)需通過(guò) fx.In 和 fx.Out 的封裝能力來(lái)實(shí)現(xiàn)了,非常簡(jiǎn)潔。 當(dāng)然,如果我們上面的返回值直接就是 interface,那么久不需要 fx.As 這一步轉(zhuǎn)換了。 go復(fù)制代碼func NewGateway(ro, rw *db.Conn) *Gateway { ... } fx.Provide( ? fx.Annotate( ? ? NewGateway, ? ? fx.ParamTags(`name:"ro" optional:"true"`, `name:"rw"`), ? ? fx.ResultTags(`name:"foo"`), ? ), )
和下面的實(shí)現(xiàn)是等價(jià)的:
type params struct { ? fx.In ? RO *db.Conn `name:"ro" optional:"true"` ? RW *db.Conn `name:"rw"` } type result struct { ? fx.Out ? GW *Gateway `name:"foo"` } fx.Provide(func(p params) result { ? ?return result{GW: NewGateway(p.RO, p.RW)} })
這里需要注意存在兩個(gè)限制:
Annotate 不能應(yīng)用于包含 fx.In 和 fx.Out 的函數(shù),它的存在本身就是為了簡(jiǎn)化;
不能在一個(gè) Annotate 中多次使用同一個(gè)注解,比如下面這個(gè)例子會(huì)報(bào)錯(cuò):
fx.Provide( fx.Annotate( NewGateWay, fx.ParamTags(`name:"ro" optional:"true"`), fx.ParamTags(`name:"rw"), // ERROR: ParamTags was already used above fx.ResultTags(`name:"foo"`) ) )
小結(jié)
這里是 uber/fx 的理論篇,我們了解了 fx 的核心概念和基礎(chǔ)用法。和 wire 一樣,它們都要求強(qiáng)制編寫構(gòu)造函數(shù),有額外的編碼成本。但好處在于功能全面、設(shè)計(jì)比較優(yōu)雅,對(duì)業(yè)務(wù)代碼無(wú)侵入。
下一篇,我們會(huì)從實(shí)戰(zhàn)的角度,基于 cloudwego 社區(qū)的 Kitex 框架,看看怎么基于 uber/fx 實(shí)現(xiàn)優(yōu)雅的注入,敬請(qǐng)期待。
以上就是 Golang 依賴注入經(jīng)典解決方案 uber/fx 理論篇的詳細(xì)內(nèi)容,更多關(guān)于 Golang 依賴注入經(jīng)典解決方案 uber/fx 理論篇的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語(yǔ)言中三個(gè)輸入函數(shù)(scanf,scan,scanln)的區(qū)別解析
本文詳細(xì)介紹了Go語(yǔ)言中三個(gè)輸入函數(shù)Scanf、Scan和Scanln的區(qū)別,包括用法、功能和輸入終止條件等,本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-10-10Go語(yǔ)言集成開發(fā)環(huán)境之VS Code安裝使用
VS Code是微軟開源的一款編輯器,插件系統(tǒng)十分的豐富,下面介紹如何用VS Code搭建go語(yǔ)言開發(fā)環(huán)境,需要的朋友可以參考下2021-10-10簡(jiǎn)單談?wù)凣olang中的字符串與字節(jié)數(shù)組
這篇文章主要給大家介紹了關(guān)于Golang中字符串與字節(jié)數(shù)組的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用Golang具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03golang實(shí)現(xiàn)aes-cbc-256加密解密功能
這篇文章主要介紹了golang實(shí)現(xiàn)aes-cbc-256加密解密功能,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-04-04在?Go?語(yǔ)言中使用?regexp?包處理正則表達(dá)式的操作
正則表達(dá)式是處理字符串時(shí)一個(gè)非常強(qiáng)大的工具,而?Go?語(yǔ)言的?regexp?包提供了簡(jiǎn)單而強(qiáng)大的接口來(lái)使用正則表達(dá)式,本文將介紹如何在?Go?中使用?regexp?包來(lái)編譯和執(zhí)行正則表達(dá)式,以及如何從文本中匹配和提取信息,感興趣的朋友一起看看吧2023-12-12Go和Java算法詳析之分?jǐn)?shù)到小數(shù)
這篇文章主要給大家介紹了關(guān)于Go和Java算法詳析之分?jǐn)?shù)到小數(shù)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-08-08Go語(yǔ)言實(shí)現(xiàn)JSON解析的神器詳解
php轉(zhuǎn)go是大趨勢(shì),越來(lái)越多公司的php服務(wù)都在用go進(jìn)行重構(gòu),重構(gòu)過(guò)程中,會(huì)發(fā)現(xiàn)php的json解析操作是真的香。本文和大家分享了一個(gè)Go語(yǔ)言實(shí)現(xiàn)JSON解析的神器,希望對(duì)大家有所幫助2023-01-01go引入自建包名報(bào)錯(cuò):package?XXX?is?not?in?std解決辦法
這篇文章主要給大家介紹了go引入自建包名報(bào)錯(cuò):package?XXX?is?not?in?std的解決辦法,這是在寫測(cè)試引入包名的時(shí)候遇到的錯(cuò)誤提示,文中將解決辦法介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12