深入理解Golang 编程思维和工程实战
| 導語?Golang 的一些編程思維和思想,以及總結一些常見的優雅編程實戰技巧
目錄
一?Golang 編程思維
二?Golang 高級編碼技巧
1?優雅的實現構造函數編程思想
2?優雅的實現繼承編程思想
3?優雅的實現虛多態編程思想
4?Golang 的 model service 模型【類MVC模型】
5?Golang 單例模式
6?Golang layout
7?cmd & command & flag
一 Golang 編程思維
首先,我們先來看下最基本的,就是 Golang 的學習技巧,比如, 通讀 Golang 的一些好的文章如?Frequently Asked Questions (FAQ)[1]或者看看?FAQ 的中文翻譯[2]?,主要是了解 Golang 的全貌。
?Go 精華文章列表[3]
?Go 相關博客列表[4]
?Go Talks[5]。
要通讀 golang 官方的編碼規范,主要是要參考官方的?CodeReviewComments[6]?和?Effective Go[7]?這兩篇官方文章,真的非常推薦必須要好好的看完、看懂這兩篇文章(英文不好的同學可以看中文翻譯文檔),然后按照官方編碼規范來具體 coding。
?主要是能夠在具體的編碼中有跡可循.
參考業界大牛們的代碼,主要是看一些開源的優質的項目,比如 Google 他們這幫人自己搞的 Kubernetes、Istio,還有一些好的項目如 Docker、CoreDNS、etcd 等等
?項目基本架構的組織?代碼基本的編碼封裝?代碼的基本原則規范?并發的設計思想?面向對象編程的設計思想?可擴展性的設計思想
然后就是實踐,實實在在的跑一些代碼示例,可以自己建立一個 base-code 的項目,里面就是你的各種示例,然后進行一些修改、執行。
?具體的代碼示例可以從官方文檔上來,推薦Go by Example[8],里面有大量非常好的例子。?也可以自己網上隨便搜下,重要的自己要修改并執行,查看和分析結果?Go 101[9]
其次,要理解 Golang 編程思維,首先要理解 Golang 這門語言的創始初衷,初衷就是為了解決好 Google 內部大規模高并發服務的問題,主要核心就是圍繞高并發來開展;并且同時又不想引入面向對象那種很復雜的繼承關系。
首先,就是可以方便的解決好并發問題(包括高并發),那么就需要有并發思維,能夠并發處理就通過并發來進行任務分配
?這個就是涉及到了 context、 goroutine、channel(select)?可以創建大量 goroutine, 但是需要能通過 context、 channel 建立 "父子"關系,保證子任務可以能夠被回收、被主動控制(如 殺死)
再者,面向對象編程思想,利用好 interface、 struct 來實現繼承、多態的用法
?struct 匿名組合來實現繼承?interface 和 struct 來實現多態?interface 定義接口,盡可能的保持里面的方法定義簡單,然后多個 interface 進行組合
?然后,理解 Golang 語言本身的一些特性:
強類型,語法上要注意處理
GC,實際中要觀察 GC 日志并分析
注意語法語義盡可能的簡單、保持各種類型定義盡可能精簡
最后,從 Golang 社區的一些最佳實踐來看,Golang 的各種組件需要盡可能的精簡。
?Golang 中用好的一些開源組件庫,都是比較輕量級的,然后可以各自隨意組合來達到最佳實踐。?我們自己進行組件封裝、模塊封裝的時候,也是保持這個原則,盡可能的精簡,然后使用方進行組合。
二 Golang 高級編碼技巧
1 優雅的實現構造函數編程思想
一個更為優雅的構造函數的實現方式 參考: https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html 通過這個方式可以方便構造不同對象,同時避免了大量重復代碼 */ package?main import ( "fmt" "time" "golang.org/x/net/context" )type Cluster struct { opts options }type options struct { connectionTimeout time.Duration readTimeout time.Duration writeTimeout time.Duration logError func(ctx context.Context, err error) }// 通過一個選項實現為一個函數指針來達到一個目的:設置選項中的數據的狀態 // Golang函數指針的用法 type Option func(c *options)// 設置某個參數的一個具體實現,用到了閉包的用法。 // 不僅僅只是設置而采用閉包的目的是為了更為優化,更好用,對用戶更友好 func LogError(f func(ctx context.Context, err error)) Option { return func(opts *options) { opts.logError = f } }// 對關鍵數據變量的賦值采用一個方法來實現而不是直接設置 func ConnectionTimeout(d time.Duration) Option { return func(opts *options) { opts.connectionTimeout = d } } func WriteTimeout(d time.Duration) Option { return func(opts *options) { opts.writeTimeout = d } } func ReadTimeout(d time.Duration) Option { return func(opts *options) { opts.readTimeout = d } } // 構造函數具體實現,傳入相關Option,new一個對象并賦值 // 如果參數很多,也不需要傳入很多參數,只需要傳入opts ...Option即可 func NewCluster(opts ...Option) *Cluster { clusterOpts := options{} for _, opt := range opts { // 函數指針的賦值調用 opt(&clusterOpts) } cluster := new(Cluster) cluster.opts?=?clusterOpts return cluster } func?main()?{ // 前期儲備,設定相關參數 commonsOpts := []Option{ ConnectionTimeout(1 * time.Second), ReadTimeout(2 * time.Second), WriteTimeout(3 * time.Second), LogError(func(ctx context.Context, err error) { }), } // 終極操作,構造函數 cluster?:=?NewCluster(commonsOpts...) // 測試驗證 fmt.Println(cluster.opts.connectionTimeout) fmt.Println(cluster.opts.writeTimeout) }除了構造函數這個思想之外,還有一個思想,就是我們要善于利用 struct 封裝對象方法,然后再 new 一個對象出來,如下:? ?
type Cluster struct {opts options } func NewCluster(opts ...Option) *Cluster {....cluster := new(Cluster) cluster.opts = clusterOptsreturn cluster }2 優雅的實現繼承編程思想
Golang 里面沒有 C++ 、Java 那種繼承的實現方式,但是,我們可以通過 Golang 的匿名組合來實現繼承,這里要注意,這個是實際編程中經常用到的一種姿勢。具體實現就是一個 struct 里面包含一個匿名的 struct,也就是通過匿名組合,這最基礎的基類就是一個 struct 結構,然后定義相關成員變量,然后再定義一個子類,也是一個 struct,里面包含前面的 struct,即可實現繼承。
示例代碼如下,這個是我實際項目(大型 IM 架構)中的實現方式,代碼里面有詳細的解釋:
package main import ( "fmt" )// 【基類】 //定義一個最基礎的struct類MsgModel,里面包含一個成員變量msgId type MsgModel struct { msgId int msgType int }// MsgModel的一個成員方法,用來設置msgId func (msg *MsgModel) SetId(msgId int) { msg.msgId = msgId }func (msg *MsgModel) SetType(msgType int) { msg.msgType = msgType }//【子類】 // 再定義一個struct為GroupMsgModel,包含了MsgModel,即組合,但是并沒有給定MsgModel任何名字,因此是匿名組合 type GroupMsgModel struct { MsgModel// 如果子類也包含一個基類的一樣的成員變量,那么通過子類設置和獲取得到的變量都是基類的 msgId int }func (group *GroupMsgModel) GetId() int { return group.msgId }/* func (group *GroupMsgModel) SetId(msgId int) { group.msgId = msgId } */ func main() { group := &GroupMsgModel{}group.SetId(123) group.SetType(1)fmt.Println("group.msgId =", group.msgId, "\tgroup.MsgModel.msgId =", group.MsgModel.msgId) fmt.Println("group.msgType =", group.msgType, "\tgroup.MsgModel.msgType =", group.MsgModel.msgType) }3 優雅的實現虛多態編程思想
面向對象編程中,我們很多情況下,都會定義一個虛基類,然后利用多態去實現各種相似的場景或者說任務。
Golang 里面可以通過 interface + struct 來實現虛基類的用法。interface 用來定義一個 "虛基類",然后一個 struct 結構定義,用來實現這個 interface 中定義的方法,并且可以有多個類似的 struct 來實現這個 interface,只要實現了這個 interface 中定義的方法即可。這也是典型的多態的一種編程思想,也就是說 Golang 通過接口去實現了多態。
具體流程如下,這個是我實際項目(大型 IM 架構)中的實現方式:定義一個 interface 接口 MsgModel,包含了一些方法,這個就相當于 "虛基類"type MsgModel":
interface {Persist(context context.Context, msg interface{}) boolPersistOnSensitive(context context.Context, session_type, level, SensitiveStatus int32, msg interface{}) bool }定義一個類型 ?msgModelImpl struct{},用來實現上面的 interface 接口:
定義一個struct用來實現接口類型 type?msgModelImpl?struct{} 定義一個變量MsgModelImpl等于msgModelImpl,相當于可以通過MsgModelImpl來調用msgModelImpl的成員 var MsgModelImpl = msgModelImpl{}實現接口的兩個方法 func (m msgModelImpl) Persist(context context.Context, msgIface interface{}) bool { // 具體實現省略 }func (m msgModelImpl) UpdateDbContent(context context.Context, msgIface interface{}) bool { // 具體實現省略 }再定義一個 struct 類型的 msgService,包含上述接口類型 MsgModel,相當于組合了。這樣的話,這個類型就需要要實現接口方法:
type msgService struct {msgModel MsgModel }再定義一個變量 MsgService,首字母大寫,并且賦值為 msgService 對象,同時給成員 msgModel 賦值為上述已經實現了接口的 struct 對象 MsgModelImpl。
將上述已經實現接口類型的類型(MsgModelImpl) 賦值給此變量(此變量并且要是包含了接口類型的類型), 然后這個變量就可以供外部調用 var MsgService = msgService{msgModel: MsgModelImpl, }這樣就全部實現了,后面只要通過 MsgService 中的接口方法就可以調用 interface 中定義的方法,注意,定義個 MsgService,里面的成員變量 msgModel 賦值為 MsgModelImpl 的目的是為了做封裝,對外暴露接口的都是 MsgService,隱藏了內部具體的 MsgModelImpl 實現。
小結
MsgModel 是一個interface
interface 是一組抽象方法的集合,interface 未具體實現的方法,僅包含方法名參數返回值的方法
msgModelImpl 是一個struct,它實現了 MsgModel 這個interface 的所有方法
如果實現了 interface 中的所有方法,即該類/對象就實現了該接口
MsgModelImpl 是 msgModelImpl 這個 struct 的對象
msgService 是一個 struct,它包含了 MsgModel,相當于組合
MsgService 是 msgService 這個 struct 的對象,并對成員變量賦值
后面就通過 MsgService 對外提供服務,隱藏內部具體的 MsgModelImpl 實現。
4 Golang 的 model service 模型【類MVC模型】
在一個項目工程中,為了使得代碼更優雅,需要抽象出一些模型出來,同時基于C++面向對象編程的思想,需要考慮到一些類、繼承相關。在Golang中,沒有類、繼承的概念,但是我們完全可以通過struct和interface來建立我們想要的任何模型。在我們的工程中,抽象出一種我自認為是類似MVC的模型,但是不完全一樣,個人覺得這個模型抽象的比較好,容易擴展,模塊清晰。對于使用java和PHP編程的同學對這個模型應該是再熟悉不過了,我這邊通過代碼來說明下這個模型
首先一個model包,通過interface來實現,包含一些基礎方法,需要被外部引用者來具體實現
再定義一個msg包,用來具體實現model包中MsgModel模型的所有方法
model 和 具體實現方定義并實現ok后,那么就還需要一個service來統籌管理
調用訪問
總結一下,model 對應 MVC 的 M,service 對應 MVC 的 C, 調用訪問的地方對應 MVC 的 ?V。
5 ?Golang 單例模式
單例模式是一種常用的軟件設計模式,在它的核心結構中只包含一個被稱為單例的特殊類,通過單例模式可以保證系統中一個類有且僅有一個實例且該實例可以被外界訪問。
在 Golang 中有一種非常優雅的姿勢可以實現,就是通過 sync.Once 來實現,這個也是我在實際項目中所應用的,示例如下:
import "github.com/dropbox/godropbox/singleton"var SingleService = singleton.NewSingleton(func() (interface{}, error) {return &singleMsgProxy{MsgModel: msg.MsgModelImpl,}, nil })singleton.NewSingleton 就是具體單例模式的實現,然后賦值給 SingleService,這樣,在程序中任何需要獲取這個對象的時候,就直接通過 SingleService 來調用,這個調用,系統會保證,里面的 singleMsgProxy 只會被初始化對象一次,這個 singleMsgProxy 就是 new 了一個對象,并且這個對象是只需要被初始化一次的。
6 Golang layout
Golang 工程 Layout 規范,網上有較多探討,每個人的理解也會不一致,但是有些基礎的理解是可以保持統一的:
cmd
main 函數文件目錄,這個目錄下面,每個文件在編譯之后都會生成一個可執行的文件。如果只有一個app文件,那就是 main.go。這里面的代碼盡可能簡單。
conf
配置文件,如 toml、yaml 等文件
config
配置文件的解析
docs
文檔
pkg
底層各種實現,每一種實現封裝一個文件夾
業界知名開源項目如 Kubernetes、Istio 都是這樣的姿勢
build
編譯腳本
CI 腳本
上下線腳本
vendor
依賴庫
一個簡單示例如下:
$ tree -d -L 2 ├── build ├── cmd │ ├── apply │ └── check ├── conf ├── config ├── docs ├── pkg │ ├── apply │ ├── check │ ├── files │ ├── k8s │ └── options └── vendor7 cmd & command & flag
大家看 Kubernetes 的源碼就可以發現,會有這么一個現象,Kubernetes 中會有很多二進制程序,然后每個程序,可能會有不同的指令,然后每個指令都會有很多命令行參數。如果大家對 Kubernetes 有一定了解,那么就知道 kubectl 會有如下命令:
kubectl apply -f 進行部署 kubectl delete -f 刪除部署 kubectl get pod 獲取 Pod那么 kubectl 這個二進制程序,如何能夠優雅的支持不同的參數呢?
下面,還是以我實際項目工程中的應用為例,來進行演示。效果如下,程序 example 包含兩個命令 apply 和 check,還有一個 help 命令:
$ ./example Usage:example[command]Available Commands:apply apply request by json filecheck check request validity by json filehelp Help about any commandFlags:--config string config file[/.xx.yaml] (default "none")-h, --help help for example--mode string mode[cpu or all] (default "cpu")Use "example[command] --help" for more information about a command.代碼示例如下,main 入口:
package mainimport (log "github.com/sirupsen/logrus""github.com/spf13/cobra""github.com/spf13/pflag""os""example/cmd/apply""example/cmd/check""example/config" ) func main() {var cmdCheck = check.NewVPARequestCheck()var cmdApply = apply.NewVPARequestApply()var rootCmd = &cobra.Command{Use: "example"}flags := rootCmd.PersistentFlags()addFlags(flags)rootCmd.AddCommand(cmdApply, cmdCheck)if err := rootCmd.Execute(); err != nil {panic(err)} }func addFlags(flags *pflag.FlagSet) {flags.StringVar(&config.Cfg.KubeConfig, "config", "none", "config file[/.xx.yaml]")flags.StringVar(&config.Cfg.Mode, "mode", "cpu", "mode[cpu or all]") }check 命令實現如下,具體 check 相關的 Run 方法忽略:
package checkimport ( "fmt"log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "example/config" "example/pkg/check" "example/pkg/files" )type RequestCheckOptions struct {configPath string }func NewRequestCheckOptions() *RequestCheckOptions {o := &RequestCheckOptions{}return o }func NewVPARequestCheck() *cobra.Command {o := NewRequestCheckOptions()cmd := &cobra.Command{Use: "check [json file]",Short: "check request validity by json file",Long: "check request by new request json file",Args: cobra.MinimumNArgs(1),RunE: func(c *cobra.Command, args []string) error { if err := o.Run(args); err != nil { return err} return nil},}return cmd }apply 命令如下,具體 apply 相關的 Run 方法忽略: package applyimport ( "fmt" "github.com/spf13/cobra" "example/pkg/apply" "example/pkg/files" )type RequestApplyOptions struct {configPath string }func NewRequestApplyOptions() *RequestApplyOptions {o := &RequestApplyOptions{}return o }func NewVPARequestApply() *cobra.Command {o := NewRequestApplyOptions()cmd := &cobra.Command{Use: "apply [json file]",Short: "apply request by json file",Long: "apply request by new request json file",Args: cobra.MinimumNArgs(1),RunE: func(c *cobra.Command, args []string) error { if err := o.Run(args); err != nil { return err} return nil},} return cmd }然后只需要在各自的 Run 方法中實現對應的邏輯即可。
References
[1]?Frequently Asked Questions (FAQ):?https://golang.org/doc/faq
[2]?FAQ 的中文翻譯:?http://www.qaulau.com/books/Golang_Tutorial/go_programming_faq.html
[3]?Go 精華文章列表:?https://github.com/golang/go/wiki/Articles
[4]?Go 相關博客列表:?https://github.com/golang/go/wiki/Blogs
[5]?Go Talks:?https://github.com/golang/go/wiki/GoTalks
[6]?CodeReviewComments:?https://github.com/golang/go/wiki/CodeReviewComments
[7]?Effective Go:?https://golang.org/doc/effective_go.html
[8]?Go by Example:?https://gobyexample.com/
[9]?Go 101:?https://go101.org/article/101.html
最后
歡迎大家加入極客星球,后端大本營,成為后臺開發專家,大家相互學習,共同進步,讓你物有所值!?希望幫助大家薪資翻倍!
二期線上直播分享內容:
● IT行業發展現狀 ● 編程語言的(go,C++,java,python等)選擇,學習路線,大廠現狀 ● 如何訓練扎實基本功
● 如何學習和攻破網絡:關于網絡的一切知識,網絡協議(TCP和UDP核心知識),網絡編程(阻塞/非阻塞,同步/異常,reuse address/port,epoll/selecty底層原理,Reactor模型,C10M高并發高性能編程),網絡架構,協議棧,網絡排障等。
詳細參考:極客分享
- END -
看完一鍵三連在看,轉發,點贊
是對文章最大的贊賞,極客重生感謝你
推薦閱讀
深入理解Go底層原理剖析 (送書)
一文搞懂JAVA與GO垃圾回收
后端技術趨勢指南|如何選擇自己的技術方向
總結
以上是生活随笔為你收集整理的深入理解Golang 编程思维和工程实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 精美技术图赏-技术精华|二期
- 下一篇: 深入理解虚拟化