Golang定時(shí)任務(wù)框架GoCron的源碼分析
背景說(shuō)明
最近工作上有個(gè)開(kāi)發(fā)定時(shí)任務(wù)的需求,調(diào)研一下后發(fā)現(xiàn)Golang并沒(méi)有十分完善的定時(shí)任務(wù)庫(kù)。
整理我這邊的需求如下:
- 支持啟動(dòng)僅定時(shí)執(zhí)行一次的任務(wù);
- 任務(wù)在執(zhí)行之前可以完成撤銷(xiāo);
- 服務(wù)重啟之后,未完成的定時(shí)任務(wù)需要允許重新調(diào)度;
顯然,現(xiàn)成的cron庫(kù)無(wú)法滿足我的需求。限定于工期,最終自己實(shí)現(xiàn)了一個(gè)粗糙的事件驅(qū)動(dòng)定時(shí)器。
但這個(gè)事件驅(qū)動(dòng)定時(shí)器具有以下的缺點(diǎn):
- 事件訂閱/通知機(jī)制不成熟
- 無(wú)法適用于更靈活的場(chǎng)景,例如多節(jié)點(diǎn)的分布式任務(wù)調(diào)度執(zhí)行
- 模塊之間的職責(zé)不清晰,例如其實(shí)Timer模塊是Scheduler調(diào)度器的一部分,Event定時(shí)器相關(guān)的部分也是Scheduler調(diào)度器的一部分,而Executor執(zhí)行模塊也存在任務(wù)調(diào)度的功能,實(shí)際上它只需要負(fù)責(zé)完成調(diào)度器交給它的任務(wù)就好
- 沒(méi)有設(shè)計(jì)任務(wù)調(diào)度池,也就是但凡新建計(jì)劃任務(wù),就會(huì)在后臺(tái)啟動(dòng)一個(gè)協(xié)程持續(xù)監(jiān)聽(tīng);一旦任務(wù)數(shù)量太多,后臺(tái)停留的協(xié)程會(huì)越來(lái)越多,進(jìn)程總的消耗就會(huì)變得非常夸張,非常可怕
- 任務(wù)調(diào)度時(shí)不存在優(yōu)先級(jí)的概念,假如相同時(shí)間內(nèi)有多個(gè)任務(wù)同時(shí)執(zhí)行,哪個(gè)任務(wù)被優(yōu)先調(diào)度完全取決于GMP的系統(tǒng)調(diào)度
綜上,我需要著重考察現(xiàn)有的Golang任務(wù)調(diào)度框架,對(duì)任務(wù)定時(shí)器進(jìn)行重新設(shè)計(jì)。
GoCron任務(wù)調(diào)度庫(kù)
調(diào)用實(shí)例
package main import ( "fmt" "time" "github.com/jasonlvhit/gocron" ) func task() { fmt.Println("I am running task.") } func taskWithParams(a int, b string) { fmt.Println(a, b) } func main() { // Do jobs without params gocron.Every(1).Second().Do(task) gocron.Every(2).Seconds().Do(task) gocron.Every(1).Minute().Do(task) gocron.Every(2).Minutes().Do(task) gocron.Every(1).Hour().Do(task) gocron.Every(2).Hours().Do(task) gocron.Every(1).Day().Do(task) gocron.Every(2).Days().Do(task) gocron.Every(1).Week().Do(task) gocron.Every(2).Weeks().Do(task) // Do jobs with params gocron.Every(1).Second().Do(taskWithParams, 1, "hello") // Do jobs on specific weekday gocron.Every(1).Monday().Do(task) gocron.Every(1).Thursday().Do(task) // Do a job at a specific time - 'hour:min:sec' - seconds optional gocron.Every(1).Day().At("10:30").Do(task) gocron.Every(1).Monday().At("18:30").Do(task) gocron.Every(1).Tuesday().At("18:30:59").Do(task) // Begin job immediately upon start gocron.Every(1).Hour().From(gocron.NextTick()).Do(task) // Begin job at a specific date/time t := time.Date(2019, time.November, 10, 15, 0, 0, 0, time.Local) gocron.Every(1).Hour().From(&t).Do(task) // NextRun gets the next running time _, time := gocron.NextRun() fmt.Println(time) // Remove a specific job gocron.Remove(task) // Clear all scheduled jobs gocron.Clear() // Start all the pending jobs <- gocron.Start() // also, you can create a new scheduler // to run two schedulers concurrently s := gocron.NewScheduler() s.Every(3).Seconds().Do(task) <- s.Start() }
項(xiàng)目分析
這個(gè)工具庫(kù)僅有三個(gè)文件:
代碼主要分為job和scheduler兩個(gè)文件,gocron僅放置了回調(diào)方法和公共方法。項(xiàng)目整體架構(gòu)如下:
gocron通過(guò)scheduler維護(hù)一個(gè)job列表,指定MAXJOBNUM最大工作隊(duì)列,限制可執(zhí)行的工作數(shù)大小。
// gocron/scheduler.go // Scheduler struct, the only data member is the list of jobs. // - implements the sort.Interface{} for sorting jobs, by the time nextRun type Scheduler struct { jobs [MAXJOBNUM]*Job // Array store jobs size int // Size of jobs which jobs holding. loc *time.Location // Location to use when scheduling jobs with specified times }
這里需要更正一下,并不是全局列表,僅僅只是跟隨調(diào)度器的生命周期。實(shí)際上,代碼確實(shí)存在全局的默認(rèn)調(diào)度器:
var ( defaultScheduler = NewScheduler() )
因此,可以直接調(diào)用。當(dāng)然也支持實(shí)例化自己的調(diào)度器:
s := gocron.NewScheduler() s.Every(3).Seconds().Do(task) <- s.Start()
gocron是典型的鏈?zhǔn)秸{(diào)用,scheduler對(duì)象通過(guò)返回job對(duì)象,完成job對(duì)象的封裝操作之后,加入調(diào)度器內(nèi)部的jobs列表,再通過(guò)Start方法啟動(dòng)調(diào)度器監(jiān)控協(xié)程,輪詢(xún)列表中的jobs,一旦找到可執(zhí)行的任務(wù),就會(huì)啟動(dòng)協(xié)程運(yùn)行job的Func對(duì)象。
// Job struct keeping information about job type Job struct { interval uint64 // pause interval * unit between runs jobFunc string // the job jobFunc to run, func[jobFunc] //...... funcs map[string]interface{} // Map for the function task store fparams map[string][]interface{} // Map for function and params of function //...... }
funcs維護(hù)一個(gè)map,緩存funcName到func的映射關(guān)系。具體封裝在Do方法:
// gocron/job.go // func (j *Job) Do(jobFun interface{}, params ...interface{}) error fname := getFunctionName(jobFun) j.funcs[fname] = jobFun j.fparams[fname] = params j.jobFunc = fname
在執(zhí)行任務(wù)時(shí),通過(guò)反射回調(diào)func:
// gocron/job.go // func (j *Job) run() ([]reflect.Value, error) result, err := callJobFuncWithParams(j.funcs[j.jobFunc], j.fparams[j.jobFunc]) if err != nil { return nil, err } // gocron/gocron.go func callJobFuncWithParams(jobFunc interface{}, params []interface{}) ([]reflect.Value, error) { f := reflect.ValueOf(jobFunc) if len(params) != f.Type().NumIn() { return nil, ErrParamsNotAdapted } in := make([]reflect.Value, len(params)) for k, param := range params { in[k] = reflect.ValueOf(param) } return f.Call(in), nil }
啟動(dòng)調(diào)度器時(shí),啟動(dòng)監(jiān)控協(xié)程:
// Start all the pending jobs // Add seconds ticker func (s *Scheduler) Start() chan bool { stopped := make(chan bool, 1) // ticker每秒產(chǎn)生一個(gè)信號(hào) ticker := time.NewTicker(1 * time.Second) go func() { for { // select選擇器阻塞 // case接收到信號(hào)則執(zhí)行 // 同時(shí)接收到多個(gè)信號(hào)則隨機(jī)選擇一個(gè)執(zhí)行 select { // ticker每秒產(chǎn)生一次信號(hào) // RunPending輪詢(xún)jobs列表,尋找到了時(shí)間可執(zhí)行的任務(wù) case <-ticker.C: s.RunPending() // stopped接收到停止信號(hào),退出調(diào)度器協(xié)程 case <-stopped: ticker.Stop() return } } }() return stopped }
一個(gè)調(diào)度器一個(gè)協(xié)程,通過(guò)統(tǒng)一的調(diào)度協(xié)程去監(jiān)控調(diào)度器任務(wù)列表內(nèi)的任務(wù)。
// RunPending runs all the jobs that are scheduled to run. func (s *Scheduler) RunPending() { // 輪詢(xún)jobs列表,找到到時(shí)間可執(zhí)行的任務(wù),創(chuàng)建可執(zhí)行任務(wù)列表 runnableJobs, n := s.getRunnableJobs() if n != 0 { for i := 0; i < n; i++ { // 啟動(dòng)協(xié)程運(yùn)行 go runnableJobs[i].run() // 刷新job執(zhí)行信息,等待下一輪調(diào)度 runnableJobs[i].lastRun = time.Now() runnableJobs[i].scheduleNextRun() } } }
綜合分析
綜上,gocron有如下好處:
- 鏈?zhǔn)秸{(diào)用簡(jiǎn)單易用
- scheduler和job職責(zé)清晰,項(xiàng)目架構(gòu)非常容易理解
- 調(diào)度器一鍵啟動(dòng)協(xié)程監(jiān)控,只有到了時(shí)間可執(zhí)行的任務(wù)才會(huì)被加入到runablejobs列表,大大減少了進(jìn)程中協(xié)程的數(shù)量,減少資源消耗
- 調(diào)度器維護(hù)的待執(zhí)行任務(wù)池,存在預(yù)設(shè)的容量大小,限定了同時(shí)可執(zhí)行的最大任務(wù)數(shù)量,不會(huì)導(dǎo)致超量
但它的缺陷也同樣明顯:
- 當(dāng)不同的線程同時(shí)對(duì)同一個(gè)調(diào)度器進(jìn)行操作,對(duì)任務(wù)列表產(chǎn)生的影響是不可預(yù)知的。因此這個(gè)框架下,最好是每個(gè)client維護(hù)自己的scheduler對(duì)象
- 雖然調(diào)度器維護(hù)一個(gè)jobs列表,但如果超過(guò)列表設(shè)定容量的任務(wù)便無(wú)法等待執(zhí)行了……這一點(diǎn)gocron并沒(méi)有理睬
- 幾乎每秒,為了找到可執(zhí)行的任務(wù)去構(gòu)建runablejobs列表,都會(huì)輪詢(xún)一次任務(wù)列表。為了追求結(jié)果的一致,它會(huì)對(duì)jobs進(jìn)行排序,雖然Golang編譯器對(duì)內(nèi)置的sort方法進(jìn)行了優(yōu)化,會(huì)選舉最快的方式對(duì)數(shù)據(jù)進(jìn)行處理,但依然存在消耗
- 依然是內(nèi)存操作,服務(wù)重啟任務(wù)列表就不存在了。也沒(méi)有考慮到多節(jié)點(diǎn)的場(chǎng)景。
新的GoCron分析
https://github.com/go-co-op/gocron
原gocron的作者居然住進(jìn)ICU了,管理員說(shuō)截止至2020年3月依然無(wú)法聯(lián)系上他。愿他身體安康……gocron被fork后有了新的發(fā)展,趕緊扒下來(lái)學(xué)習(xí)一下
新的gocron新增了很多內(nèi)容,依然圍繞著Scheduler和Job進(jìn)行鏈?zhǔn)讲僮鳎略隽薳xecutor模塊。executor僅負(fù)責(zé)執(zhí)行Scheduler調(diào)度過(guò)來(lái)的任務(wù)。
項(xiàng)目架構(gòu)
下面是項(xiàng)目README文檔里公開(kāi)的架構(gòu)圖:
新功能
新版gocron支持了cron格式的語(yǔ)法
// cron expressions supported s.Cron("*/1 * * * *").Do(task) // every minute
新增了異步和阻塞模式的兩種調(diào)度方式
// you can start running the scheduler in two different ways: // starts the scheduler asynchronously s.StartAsync() // starts the scheduler and blocks current execution path s.StartBlocking()
通過(guò)設(shè)置信號(hào)量限制可同時(shí)運(yùn)行的任務(wù)數(shù)量
// gocron/scheduler.go // SetMaxConcurrentJobs limits how many jobs can be running at the same time. // This is useful when running resource intensive jobs and a precise start time is not critical. func (s *Scheduler) SetMaxConcurrentJobs(n int, mode limitMode) { // 通過(guò)對(duì)n的配置修改并發(fā)任務(wù)數(shù)的大小 s.executor.maxRunningJobs = semaphore.NewWeighted(int64(n)) // limitMode即當(dāng)可執(zhí)行任務(wù)達(dá)到最大并發(fā)量時(shí),應(yīng)該如何處理的邏輯 // RescheduleMode:跳過(guò)本次執(zhí)行,等待下一次調(diào)度 // WaitMode:持續(xù)等待,知道可執(zhí)行隊(duì)列空出。但,由于等待的任務(wù)數(shù)積累,可能導(dǎo)致不可預(yù)知的后果,某些任務(wù)可能一直等不到執(zhí)行 s.executor.limitMode = mode } // gocron/executor.go // 通過(guò)信號(hào)量的方式從最大數(shù)量中取一位 // 若通過(guò),下一步可以執(zhí)行函數(shù) if e.maxRunningJobs != nil { if !e.maxRunningJobs.TryAcquire(1) { switch e.limitMode { case RescheduleMode: return case WaitMode: select { case <-stopCtx.Done(): return case <-f.ctx.Done(): return default: } if err := e.maxRunningJobs.Acquire(f.ctx, 1); err != nil { break } } } defer e.maxRunningJobs.Release(1) }
gocron支持指定Job以單例模式運(yùn)行。通過(guò)siglefilght工具庫(kù)保證當(dāng)前僅有一個(gè)可運(yùn)行的Job
// gocron/job.go // SingletonMode prevents a new job from starting if the prior job has not yet // completed it's run // Note: If a job is added to a running scheduler and this method is then used // you may see the job run overrun itself as job is scheduled immediately // by default upon being added to the scheduler. It is recommended to use the // SingletonMode() func on the scheduler chain when scheduling the job. func (j *Job) SingletonMode() { j.mu.Lock() defer j.mu.Unlock() j.runConfig.mode = singletonMode j.jobFunction.limiter = &singleflight.Group{} } // gocron/executor.go switch f.runConfig.mode { case defaultMode: runJob() case singletonMode: // limiter是singlefilght對(duì)象,Do方法內(nèi)僅會(huì)執(zhí)行一次,保證一次只運(yùn)行一個(gè)任務(wù) _, _, _ = f.limiter.Do("main", func() (interface{}, error) { select { case <-stopCtx.Done(): return nil, nil case <-f.ctx.Done(): return nil, nil default: } runJob() return nil, nil }) }
gocron主要數(shù)據(jù)結(jié)構(gòu)
主要分為schduler調(diào)度器,job任務(wù),以及executor執(zhí)行器對(duì)象
追蹤一下調(diào)用鏈的工作流程:
- 初始化一個(gè)Scheduler;新版gocron似乎更鼓勵(lì)用戶使用自己的scheduler,而不是如同老版一樣維護(hù)一個(gè)默認(rèn)的全局調(diào)度器
func NewScheduler(loc *time.Location) *Scheduler { // 這時(shí)已經(jīng)將executor同步初始化完畢 // scheduler和executor是一對(duì)一的關(guān)系 executor := newExecutor() return &Scheduler{ jobs: make([]*Job, 0), location: loc, running: false, time: &trueTime{}, executor: &executor, tagsUnique: false, timer: afterFunc, } }
- Every方法初始化一個(gè)Job,如果scheduler已經(jīng)啟動(dòng),即任務(wù)列表中已經(jīng)存在一個(gè)等待封裝的Job,那么直接取出相應(yīng)的Job
if s.updateJob || s.jobCreated { job = s.getCurrentJob() }
接下來(lái)確定Job的運(yùn)行周期,并加入到任務(wù)列表
s.setJobs(append(s.Jobs(), job))
Every方法返回了新增Job的scheduler,此時(shí)scheduler的任務(wù)隊(duì)列中存在一個(gè)Job就緒,等待下一步調(diào)度。
- Do方法帶著回調(diào)的函數(shù)和對(duì)應(yīng)的參數(shù)開(kāi)始執(zhí)行,它從當(dāng)前的scheduler中取出一個(gè)就緒的Job,進(jìn)行最后的判斷,如果Job不合格,那么將它從任務(wù)隊(duì)列中移除,并返回報(bào)錯(cuò)
if job.error != nil { // delete the job from the scheduler as this job // cannot be executed s.RemoveByReference(job) return nil, job.error } // 還有很多判斷條件,這里不一一列舉
將Do方法將要執(zhí)行的函數(shù)封裝進(jìn)Job。接下來(lái)判斷schduler是否啟動(dòng):如之前gocron一樣,scheduler也是通過(guò)協(xié)程監(jiān)聽(tīng)并執(zhí)行啟動(dòng)任務(wù)協(xié)程的工作。
之前的scheduler,默認(rèn)啟動(dòng)一個(gè)ticker,每秒去排序并輪詢(xún)?nèi)蝿?wù)隊(duì)列,從中取出滿足條件的任務(wù)開(kāi)始執(zhí)行,效率非常低。而現(xiàn)在的改進(jìn)是:scheduler啟動(dòng)監(jiān)聽(tīng)協(xié)程后;不是以輪詢(xún)而是以通知的方式,從channel中獲取Job的Function,再啟動(dòng)協(xié)程去執(zhí)行。
在這樣的前提下,scheduler監(jiān)聽(tīng)協(xié)程什么時(shí)候啟動(dòng)是位置的。此處添加一個(gè)判斷,當(dāng)scheduler啟動(dòng)時(shí),同時(shí)啟動(dòng)runContinuous去完成Job的最后一步操作。若是scheduler沒(méi)有啟動(dòng),那么直接返回,等待scheduler啟動(dòng)后再完成操作。
// we should not schedule if not running since we can't foresee how long it will take for the scheduler to start if s.IsRunning() { s.runContinuous(job) }
通過(guò)這樣的設(shè)計(jì),在最終啟動(dòng)scheduler前后,都可以以動(dòng)態(tài)的方式添加/移除任務(wù)。
- scheduler提供了兩種啟動(dòng)schduler的模式:異步和阻塞(也就是同步啦)
// StartAsync starts all jobs without blocking the current thread func (s *Scheduler) StartAsync() { if !s.IsRunning() { s.start() } } // StartBlocking starts all jobs and blocks the current thread. // This blocking method can be stopped with Stop() from a separate goroutine. func (s *Scheduler) StartBlocking() { s.StartAsync() s.startBlockingStopChanMutex.Lock() s.startBlockingStopChan = make(chan struct{}, 1) s.startBlockingStopChanMutex.Unlock() <-s.startBlockingStopChan }
一般情況下,我們通過(guò)異步模式,啟動(dòng)對(duì)所有任務(wù)的監(jiān)控
// start starts the scheduler, scheduling and running jobs func (s *Scheduler) start() { // 啟動(dòng)監(jiān)聽(tīng)協(xié)程,select選擇器配合channel阻塞 // 直到Job準(zhǔn)備執(zhí)行發(fā)送通知 go s.executor.start() // 將scheduler置位為running s.setRunning(true) // 遍歷所有任務(wù),以遞歸的方式監(jiān)控起來(lái) s.runJobs(s.Jobs()) }
比較有意思的是這個(gè)部分:
func (s *Scheduler) runJobs(jobs []*Job) { for _, job := range jobs { // 這個(gè)函數(shù)是一個(gè)遞歸調(diào)用 // 這里對(duì)所有Job都以遞歸的方式監(jiān)聽(tīng)著 s.runContinuous(job) } } // 這是runContinuous的部分代碼 job.setTimer(s.timer(nextRun, func() { if !next.dateTime.IsZero() { for { n := s.now().UnixNano() - next.dateTime.UnixNano() // 某個(gè)任務(wù)滿足執(zhí)行條件了,退出循環(huán) if n >= 0 { break } s.time.Sleep(time.Duration(n)) } } // 遞歸執(zhí)行本方法 // runContinuous會(huì)判斷當(dāng)前Job是否可執(zhí)行 // 若不則退出,若可以則將Job設(shè)置為立即執(zhí)行,并刷新執(zhí)行時(shí)間 // 若Job“立即執(zhí)行”的標(biāo)志已經(jīng)置位,直接調(diào)用run發(fā)送通知給監(jiān)聽(tīng)協(xié)程 s.runContinuous(job) }))
這樣的設(shè)計(jì)太優(yōu)雅了,大佬們的奇思妙想啊~
- 最后是executor的執(zhí)行,前面已經(jīng)提到過(guò)。通過(guò)select接收channel通知的形式執(zhí)行下去,核心方法是這個(gè):
runJob := func() { f.incrementRunState() callJobFunc(f.eventListeners.onBeforeJobExecution) callJobFuncWithParams(f.function, f.parameters) callJobFunc(f.eventListeners.onAfterJobExecution) f.decrementRunState() }
eventListeners封裝了兩個(gè)接口,用以在執(zhí)行任務(wù)和完成任務(wù)后發(fā)送給用戶事件通知。
綜合分析
gocron進(jìn)行了不少方面的優(yōu)化:
- 在任務(wù)列表的維護(hù)上,可加入調(diào)度的任務(wù)數(shù)不再限定為某個(gè)值,而是以切片的方式自動(dòng)增長(zhǎng)。但最終能夠并行執(zhí)行的任務(wù)數(shù)卻通過(guò)信號(hào)量多方式加以控制;
- 不再周期性地輪詢(xún)?nèi)蝿?wù)列表,以期待獲得可運(yùn)行的任務(wù);而是通過(guò)更巧妙的方式,任務(wù)遞歸監(jiān)聽(tīng),一旦發(fā)現(xiàn)可執(zhí)行的任務(wù),就自行通知scheduler,完成調(diào)度;
- 具備更豐富的語(yǔ)法和模式,用戶可以根據(jù)場(chǎng)景自行選擇;調(diào)度器同時(shí)支持異步及同步調(diào)用,而Job也支持周期性輪詢(xún)和單點(diǎn)任務(wù);
- scheduler內(nèi)加鎖了,對(duì)Jobs列表的操作都會(huì)加上讀寫(xiě)鎖,一些其它的參數(shù)也擁有自己的鎖。這使得scheduler具備線程安全性,但某種程度上影響了對(duì)Jobs隊(duì)列的操作??紤]到gocron不再鼓勵(lì)使用全局Scheduler,而是每個(gè)client維護(hù)自己的Scheduler,那么被鎖影響的場(chǎng)景會(huì)進(jìn)一步減少,與最終優(yōu)化獲得的性能提升相比,都是值得的。
最后
最后的最后,gocron依然無(wú)法滿足我當(dāng)前的需求,但已經(jīng)不妨礙我對(duì)源碼進(jìn)行下一步的改造:
- 我需要對(duì)Job進(jìn)行上層的封裝,并將要調(diào)用的方法和參數(shù)序列化后存入數(shù)據(jù)庫(kù),直到服務(wù)重啟時(shí),能夠找到未完成的任務(wù)加載進(jìn)scheduler重新執(zhí)行
- 我的計(jì)劃任務(wù)只需要執(zhí)行一次,而無(wú)須重復(fù)執(zhí)行,這一點(diǎn)已經(jīng)有SingletonMode保證
- 我需要改造gocron,讓它能夠支持在某個(gè)時(shí)間范圍內(nèi)調(diào)度任務(wù)
到此這篇關(guān)于Golang定時(shí)任務(wù)框架GoCron的源碼分析的文章就介紹到這了,更多相關(guān)Golang定時(shí)任務(wù)框架GoCron內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go通過(guò)SJSON實(shí)現(xiàn)動(dòng)態(tài)修改JSON
在Go語(yǔ)言 json 處理領(lǐng)域,在 json 數(shù)據(jù)處理中,讀取與修改是兩個(gè)核心需求,本文我們就來(lái)看看如何使用SJSON進(jìn)行動(dòng)態(tài)修改JSON吧,有需要的小伙伴可以了解下2025-03-03詳解 Go 語(yǔ)言中 Map 類(lèi)型和 Slice 類(lèi)型的傳遞
這篇文章主要介紹了詳解 Go 語(yǔ)言中 Map 類(lèi)型和 Slice 類(lèi)型的傳遞的相關(guān)資料,需要的朋友可以參考下2017-09-09golang調(diào)用shell命令(實(shí)時(shí)輸出,終止)
本文主要介紹了golang調(diào)用shell命令(實(shí)時(shí)輸出,終止),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02Golang實(shí)現(xiàn)http重定向https
這篇文章介紹了Golang實(shí)現(xiàn)http重定向https的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07掌握Golang中的select語(yǔ)句實(shí)現(xiàn)并發(fā)編程
Golang中的select語(yǔ)句用于在多個(gè)通道間選擇可讀或可寫(xiě)的操作,并阻塞等待其中一個(gè)通道進(jìn)行操作??梢杂糜趯?shí)現(xiàn)超時(shí)控制、取消和中斷操作等。同時(shí),select語(yǔ)句支持default分支,用于在沒(méi)有任何通道可操作時(shí)執(zhí)行默認(rèn)操作2023-04-04