源码分析
goFrame
框架是一款国产的 golang web 框架,它事无巨细的把我们可能用到的组件都进行了封装,目前 Github star 已高达 5.7K ,人送“封装狂魔”的称号。既然如此,那我们就来学习一下别人是怎么来封装的。
我们知道 goFrame
框架的 main 文件非常简单,以下是官方模板的 main 文件示例:
package main
import (
_ "github.com/gogf/gf-demos/router"
"github.com/gogf/gf/frame/g"
)
// @title `gf-demo`示例服务API
// @version 1.0
// @description `GoFrame`基础开发框架示例服务API接口文档。
// @schemes http
func main() {
g.Server().Run()
}
正常我们拉起一个 Server ,拿到实例之后再去注册路由,比如 Gin 拉起服务是这样的:
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}
为什么 GoFrame 注册路由不需要 main 中 初始化的实例就可以直接注册路由呢?
package router
import (
"github.com/gogf/gf-demos/app/api"
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
)
func init() {
s := g.Server()
s.Group("/", func(group *ghttp.RouterGroup) {
group.ALL("/chat", api.Chat)
})
}
为什么这么简短的代码就能拉起一个 Server 服务呢?我们追踪代码来看下 Server()
方法做了些什么事情!
// 位于 github.com/gogf/gf/frame/g/g_object.go 文件 30 行
func Server(name ...interface{}) *ghttp.Server {
return gins.Server(name...)
}
Server()
方法很简单,我们继续往下追踪:
// 位于 github.com/gogf/gf/frame/gins/gins_server.go 文件
package gins
import (
"fmt"
"github.com/gogf/gf/net/ghttp"
"github.com/gogf/gf/util/gutil"
)
const (
frameCoreComponentNameServer = "gf.core.component.server"
configNodeNameServer = "server"
)
// Server returns an instance of http server with specified name.
func Server(name ...interface{}) *ghttp.Server {
instanceKey := fmt.Sprintf("%s.%v", frameCoreComponentNameServer, name)
return instances.GetOrSetFuncLock(instanceKey, func() interface{} {
s := ghttp.GetServer(name...)
// To avoid file no found error while it's not necessary.
if Config().Available() {
var m map[string]interface{}
nodeKey, _ := gutil.MapPossibleItemByKey(Config().GetMap("."), configNodeNameServer)
if nodeKey == "" {
nodeKey = configNodeNameServer
}
m = Config().GetMap(fmt.Sprintf(`%s.%s`, nodeKey, s.GetName()))
if len(m) == 0 {
m = Config().GetMap(nodeKey)
}
if len(m) > 0 {
if err := s.SetConfigWithMap(m); err != nil {
panic(err)
}
}
// As it might use template feature,
// it initialize the view instance as well.
_ = getViewInstance()
}
return s
}).(*ghttp.Server)
}
到这里也就初见端倪了,instances
是一个全局的 Map:
// 位于 github.com/gogf/gf/frame/gins/gins.go 文件
package gins
import (
"github.com/gogf/gf/container/gmap"
)
var (
// instances is the instance map for common used components.
instances = gmap.NewStrAnyMap(true)
)
GetOrSetFuncLock
则是一个加锁的操作,先检索 Map 中是否存在指定 key 的值,没有则执行传入的 func
并将返回值添加到 Map 中:
func (m *StrAnyMap) GetOrSetFuncLock(key string, f func() interface{}) interface{} {
if v, ok := m.Search(key); !ok {
return m.doSetWithLockCheck(key, f)
} else {
return v
}
}
我们知道导入包的 init
方法会先于 main
方法执行,所以当 main 文件引入的 router.go
中执行 g.Server()
时,全局 Map instances 中并没有对应的 Server,Server()
方法则会初始化一个服务并放入全局 Map 中,然后 main 方法再执行 g.Server()
获取到的则是我们在 router
文件中初始化并注册好路由的 Server。
这就是经典的设计模式 单例 的实现了,那有没有其他的方式实现单例呢?
sync.Once
sync.Once
是 golang 基础包中自带的一个方法,跟他的名字一样,代码仅执行一次。
package main
import (
"sync"
"fmt"
)
var once sync.Once
func main() {
for i := 0; i < 3; i++ {
Server()
}
}
func Server() {
once.Do(func() {
fmt.Println("new server")
})
fmt.Println("get server")
}
// new server
// get server
// get server
// get server
这也就达到了我们的要求,初始化代码只执行一次。那他是怎么实现的呢?我们来看看 sync.Once
的源码。
package sync
import (
"sync/atomic"
)
type Once struct {
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
代码其实很简单,底层其实也是加锁实现。用 done
标志位来标识是否已经执行过了,已执行就直接跳过。
这里的加锁是非常有必要的,如果不加锁,当 f 还没执行完,done 标志位还未置为 1 时,这时并发执行就会导致再次执行 f 。