[圖片上傳失敗...(image-9d243d-1592273073087)]
項(xiàng)目地址
https://github.com/LouisYZK/Frodo
博客原文地址
http://zhikai.pro/
Frodo-v2.0 沒(méi)有添加新功能山宾,而是將后端最重要的部分妨马,后臺(tái)API使用
golang
重構(gòu)白胀,python現(xiàn)在只負(fù)責(zé)前臺(tái)模板的渲染。這樣原本的單服務(wù)應(yīng)用就成了多服務(wù)。本文將簡(jiǎn)介v2.0的調(diào)整思路和golang異步的特性闯团,新版本的部署文檔請(qǐng)參看項(xiàng)目地址
{
'user': 'LouisYZK',
'repo': 'Frodo',
'right': 0
}
主要重構(gòu)的模塊為:
- 博文函匕、用戶、標(biāo)簽等的后臺(tái)CRUD接口
- 緩存清理模塊
- JWT認(rèn)證模塊
why golang?
golang是年輕的語(yǔ)言耍目,新世紀(jì)的靜態(tài)語(yǔ)言膏斤。設(shè)計(jì)理念很好地平衡了C++
和 javascript/python
等動(dòng)態(tài)語(yǔ)言的優(yōu)劣,獨(dú)具特色的goroutine
設(shè)計(jì)范式旨在告別多線程式的并發(fā)邪驮。而web后臺(tái)和微服務(wù)是go語(yǔ)言用的的最多的領(lǐng)域莫辨,故我將后臺(tái)純api部分拿go來(lái)重寫。
我是2019年開(kāi)始使用kubernetes
時(shí)開(kāi)始接觸的go語(yǔ)言毅访,當(dāng)時(shí)項(xiàng)目有需求想要擴(kuò)展一個(gè)k8s的api, 官方給的意見(jiàn)是如果想真正的contributor,最好使用golang開(kāi)發(fā)沮榜。go語(yǔ)言的代表一定是docker
和kubernetes
這一最主流的容器與容器編排工具。
其次喻粹,使用go語(yǔ)言對(duì)我并不痛苦蟆融,他的風(fēng)格介于靜態(tài)和動(dòng)態(tài)語(yǔ)言,因此你要使用指針來(lái)避免冗余的對(duì)象復(fù)制守呜,同時(shí)你也能使用便捷的如python里的動(dòng)態(tài)數(shù)據(jù)結(jié)構(gòu)型酥。比類C的好處在于他不是那么地接近底層,只需考慮必要的指針操作和類型問(wèn)題查乒。
而python也越來(lái)越多地提倡使用顯示類型弥喉,在v1.0中就已經(jīng)使用了類型檢查,在python中雖然不能帶來(lái)性能的提升玛迄,但有利于調(diào)試和對(duì)接靜態(tài)語(yǔ)言由境。因此golang的語(yǔ)并不會(huì)帶來(lái)困難。
帶類型的python與golang風(fēng)格十分相似:
async def get_user_by_id(id: int) -> User:
user: User = await User.async_first(id)
return user
在golang中類型是強(qiáng)制的:
func GetUsers(page int) (users []User) {
DB.Where(...).First(&user)
return
}
再來(lái)看C++, 明顯的不同時(shí)類型的位置不一樣:
User* getUserById(int id) {
user = User{id}
User::first(&user)
return user
}
最后蓖议,最重要的是golang的圈子如何虏杰,跟python-web比,golang可選擇的余地并不是很多拒担,但也足夠用嘹屯。這次選擇的框架是gin
和gorm
都是輕量且簡(jiǎn)單的框架。
Challenge
golang的輪子
寫習(xí)慣動(dòng)態(tài)原因的人(尤其是python/js)會(huì)感覺(jué)golang數(shù)據(jù)結(jié)構(gòu)的麻煩:
-
map/struct
不能動(dòng)態(tài)添加新屬性 - 沒(méi)有
in
這一經(jīng)常使用的特性 - 任意類型
interface{}
到其他類型的轉(zhuǎn)換并沒(méi)有那么簡(jiǎn)單 -
struct
从撼,map
,json
之間的轉(zhuǎn)換并不是很自然 - 值傳遞和地址傳遞時(shí)刻要注意
- 沒(méi)有方便的集合運(yùn)算州弟,如交并差钧栖,如排序,如格式化生成等婆翔。
- ...
慶幸的是拯杠,go語(yǔ)言的開(kāi)源社區(qū)做的很不錯(cuò),可以直接飲用github的他人完成的包啃奴,很多輪子都有現(xiàn)成的實(shí)現(xiàn)潭陪,首先可以去 https://godoc.org/
去搜索官方支持的輪子,這些一般是穩(wěn)定的最蕾,受官方認(rèn)可的依溯,同時(shí)可以方便地查看他們的文檔。如集合運(yùn)算我就使用了goset
這個(gè)庫(kù)瘟则。如果沒(méi)有在官方找到黎炉,可以直接尋求github,直接引用倉(cāng)庫(kù)地址即可醋拧。(感覺(jué)golang包模塊很方便嗎慷嗜?目前看來(lái)是的,但其實(shí)坑也不少...)
多服務(wù)網(wǎng)絡(luò)結(jié)構(gòu)部署
沒(méi)想到V2.0版本麻煩最多是在部署上...
這樣我們的博客系統(tǒng)就有兩個(gè)服務(wù)了丹壕,golang和uvicorn分別占兩個(gè)端口庆械,靜態(tài)文件中做相應(yīng)的調(diào)整,但因?yàn)槲业牟渴鹬荒鼙┞兑粋€(gè)端口(因?yàn)橛蛎麊?wèn)題菌赖,見(jiàn)下圖)缭乘,這樣只能借助nginx
來(lái)轉(zhuǎn)發(fā)了。
[圖片上傳失敗...(image-ae80af-1592273073087)]
上圖結(jié)構(gòu)有幾個(gè)配置上的難點(diǎn):
靜態(tài)資源尋址盏袄、路由配置忿峻。v1.0但語(yǔ)言版本時(shí)比較好配置直接都映射本地地址即可。現(xiàn)在需要明確地分服務(wù)在nginx配置轉(zhuǎn)發(fā)辕羽。同時(shí)靜態(tài)資源上逛尚,也要將原先的本地地址更換為域名地址。
golang部分功能還要調(diào)用python的服務(wù)刁愿,如「動(dòng)態(tài)」的api的還是保留在python里绰寞,post的 api在golang, 而創(chuàng)建「文章」后需要?jiǎng)?chuàng)建動(dòng)態(tài),這時(shí)golang需要調(diào)用python的服務(wù)铣口。(這其實(shí)很正常滤钱,很大的項(xiàng)目也避免不了互相通信的需要。)好在在一臺(tái)機(jī)器上此問(wèn)題容易解決的多脑题。
等等件缸,緩存會(huì)沖突嗎? 在「數(shù)據(jù)篇」中講到Frodo是有緩存機(jī)制的叔遂,現(xiàn)在發(fā)現(xiàn)python的前臺(tái)和golang的后臺(tái)都依賴緩存他炊,這點(diǎn)需要嚴(yán)格的key的統(tǒng)一來(lái)保證兩個(gè)緩存數(shù)據(jù)的一致性争剿。
Golang的異步與并發(fā)
既然將原來(lái)python的服務(wù)換為golang, 前面提到的異步特性golang能滿足嗎?其實(shí)思想是一致的痊末,只是從asycio和可等待對(duì)象變?yōu)榱薵oroutine, 拿「博文」創(chuàng)建接口舉例:
func CreatePost(data map[string]interface{}) {
post := new(Post)
post.Title = data["title"].(string)
post.Summary = data["summary"].(string)
post.Type = data["type"].(int)
post.CanComment = data["can_comment"].(int)
post.AuthorID = data["author_id"].(int)
post.Status = data["status"].(int)
tags := data["tags"].([]string)
content := data["content"]
DB.Create(&post)
fmt.Println(post)
go post.SetProps("content", content) // go設(shè)置內(nèi)容
go post.UpdateTags(tags) // go 更新標(biāo)簽
go post.Flush() // go 清除緩存
go CreateActivity(post) // go 創(chuàng)建動(dòng)態(tài)
}
可以看到連續(xù)使用了4個(gè)go
分發(fā)不能阻塞的任務(wù)蚕苇,這些都是goroutine
, 配套的有對(duì)他們管理的通信工具和同步原語(yǔ),每個(gè)goroutine
也可以繼續(xù)分發(fā)協(xié)程凿叠,如其中的更新標(biāo)簽:
func (post *Post) UpdateTags(tagNames []string) {
var originTags []Posttag
var originTagNames []string
DB.Where("post_id = ?", post.ID).Find(&originTags)
for _, item := range originTags {
var tag Tag
DB.Select("name").Where("id = ?", item.TagID).First(&tag)
originTagNames = append(originTagNames, tag.Name)
}
_, _, deleteTagNames, addTagNames := goset.Difference(originTagNames, tagNames)
for _, tag := range addTagNames.([]string) {
go CreateTags(tag)
go CreatePostTags(post.ID, tag)
}
for _, tag := range deleteTagNames.([]string) {
go DeletePostTags(post.ID, tag)
}
}
golang沒(méi)有類似asyncio.gather(*coros)
式的分發(fā)涩笤,采用for循環(huán)是一樣的實(shí)現(xiàn)。
目前我已經(jīng)把簡(jiǎn)單的系統(tǒng)拆成了兩個(gè)不同技術(shù)類型的服務(wù)盒件,可以見(jiàn)到部署難題漸顯蹬碧,接下來(lái)的更新就是虛擬化解決環(huán)境依賴難題和自動(dòng)化部署了~