Go项目的系统初始化设计-游戏版

Go中提供了一种初始化数据和逻辑的方式:func init() {}。当我们要初始化配置文件,或者是要启动一个goroutine,都可以在对应模块的init方法中进行。
而init方法执行的时机在于该package被第一次访问时。在程序中,我们有两种方式来触发指定package内的init方法的执行:

  1. 显示地import package来,如:import github.com/Jordanzuo/goutil/baseUtil
  2. 使用_的方式来import package,如:import _github.com/Jordanzuo/goutil/baseUtil

在第一种方式中,需要在import package的文件中显示地调用baseUtil的方法,否则会编译失败;而在第二种方式中,则没有这个限制。因为import _ package的设计目的就是为了方便在不使用package内容的情况下,让其内部的init方法得到执行。

看起来很美好。只要各个package之间互相之间没有依赖关系,则各个模块可以独自地进行数据和方法和初始化。但是在一个规模稍微大一些的系统中,模块和模块之间或多或少地存在着一些依赖关系,比如模块A依赖于系统配置模块。那么在系统配置模块初始化完成前进行模块A的初始化,则会出现空引用的异常。

这个问题如何解决呢?我们以游戏为例来说明此问题的解决方案。
在游戏中,需要进行初始化的内容包括两大类:数据和goroutine。其中:

  • 数据:游戏中的各类数据;数据内部有依赖关系。
  • goroutine:用于控制系统流程的一些独立goroutine;大多数时间都依赖于数据的初始化完成。

而数据又可以分为4类:

  1. 静态的配置数据:在整个程序的生命周期内保持不变的配置数据。
  2. 动态的配置数据:在整个程序的生命周期内会改变的配置数据。
  3. 玩家相关数据:只由玩家自己改变的数据。如玩家的装备、随从等数据。
  4. 全局数据:任何玩家都可以改变的数据。如boss、竞技场等数据。

为了保证数据的一致性和程序运行的正确性,我们对于以上的4类数据施加了一些限制:

  1. 对于静态的配置数据,我们必须在系统启动阶段,验证数据的准确性;如果发现有错误数据,则不允许程序启动。而数据准确性的保证,则依赖于模块之间数据的彼此验证。比如:装备升级表中包含了装备的模型ID,那么我们就需要保证该模型ID一定存在于装备的模型表中。
  2. 对于动态的配置数据,除了在系统启动阶段,验证的数据准确性之外;在系统运行期间,每当数据发生变化,我们在将数据应用到系统前,都需要验证数据的准确性。
  3. 对于玩家相关数据,我们一般只会将最核心的玩家数据加载到内存中,而剩余的数据则进行延迟加载。
  4. 对于全局数据,需要将全部数据都加载到内存中,并进行适当的验证。

针对以上的需求,我设计了以下的初始化管理系统。

  • 将系统的初始化分解成8个步骤:
  1. Load: 从数据库中加载数据到内存中的临时变量中。
  2. Verify: 验证临时变量中的数据的正确性;比如装备等级表中的模型ID必须存在于装备模型配置表中。
  3. Convert: 对临时变量中的数据进行必要的转换;比如将,分隔的字符串转换为[]int。
  4. Apply: 将验证和转换后的临时数据赋值给正式数据变量。
  5. Print: 打印数据。
  6. Clean: 清理临时数据。到此数据加载完成。
  7. Setup: 设置系统运行环境。例如:启动一个持续运行的goroutine。
  8. Complete: 系统初始化完成后执行的一些操作。例如:注册小红点处理逻辑。
  • 在系统中提供LoadMgr, VerifyMgr, ConvertMgr, ApplyMgr, PrintMgr, CleanMgr, SetupMgr, CompleteMgr等8个对象。
  • 在各个package的init方法中,根据实际需要,将自己内部的方法分别注册到初始化管理系统对应的对象中。如:
    bll.go
func init() {
    systemInitMgr.LoadMgr.RegisterFunc("load", load)
    systemInitMgr.VerifyMgr.RegisterFunc("verify", verify)
    systemInitMgr.ConvertMgr.RegisterFunc("convert", convert)
    systemInitMgr.ApplyMgr.RegisterFunc("apply", apply)
    systemInitMgr.PrintMgr.RegisterFunc("print", print)
    systemInitMgr.CleanMgr.RegisterFunc("clean", clean)
    systemInitMgr.SetupMgr.RegisterFunc("setup", setup)
    systemInitMgr.CompleteMgr.RegisterFunc("complete", complete)
}

func load() []error {}
func verify() []error {}
func convert() []error {}
func apply() []error {}
func print() []error {}
func clean() []error {}
func setup() []error {}
func complete() []error {}

核心代码如下:
model.go

package systemInitMgr

// 方法对象
type FuncItem struct {
    // 模块名称
    moduleName string

    // 方法定义
    funcDefinition func() []error
}

func newFuncItem(moduleName string, funcDefinition func() []error) *FuncItem {
    return &FuncItem{
        moduleName:     moduleName,
        funcDefinition: funcDefinition,
    }
}

load.go

package systemInitMgr

import (
    "strings"
    "sync"
)

var (
    LoadMgr = newLoadMgr()
)

type loadMgr struct {
    funcList []*FuncItem
    mutex    sync.Mutex
}

func newLoadMgr() *loadMgr {
    return &loadMgr{
        funcList: make([]*FuncItem, 0, 64),
    }
}

func (this *loadMgr) RegisterFunc(moduleName string, funcDefinition func() []error) {
    this.mutex.Lock()
    defer this.mutex.Unlock()

    funcItemObj := newFuncItem(moduleName, funcDefinition)
    this.funcList = append(this.funcList, funcItemObj)
}

func (this *loadMgr) CallByModuleName(moduleName string) (errList []error) {
    this.mutex.Lock()
    defer this.mutex.Unlock()

    for _, item := range this.funcList {
        if item.moduleName != moduleName {
            continue
        }

        errorList := item.funcDefinition()
        if errorList != nil && len(errorList) > 0 {
            errList = append(errList, errorList...)
        }
    }

    return
}

func (this *loadMgr) CallByModuleNamePrefix(moduleNamePrefix string) (errList []error) {
    this.mutex.Lock()
    defer this.mutex.Unlock()

    for _, item := range this.funcList {
        if strings.HasPrefix(item.moduleName, moduleNamePrefix) == false {
            continue
        }

        errorList := item.funcDefinition()
        if errorList != nil && len(errorList) > 0 {
            errList = append(errList, errorList...)
        }
    }

    return
}

func (this *loadMgr) CallAll() (errList []error) {
    this.mutex.Lock()
    defer this.mutex.Unlock()

    for _, item := range this.funcList {
        errorList := item.funcDefinition()
        if errorList != nil && len(errorList) > 0 {
            errList = append(errList, errorList...)
        }
    }

    return
}

systemInitMgr.go

package systemInitMgr

import (
    "fmt"
)

var (
    serverModuleName = "systemInitMgr"
)

func StartProcess() {
    totalErrList := make([]error, 0, 16)

    // 从数据库中加载所有的配置数据
    loadErrList := LoadMgr.CallAll()
    if len(loadErrList) > 0 {
        totalErrList = append(totalErrList, loadErrList...)
    }

    // 验证所有的数据是否合法
    verifyErrList := VerifyMgr.CallAll()
    if len(verifyErrList) > 0 {
        totalErrList = append(totalErrList, verifyErrList...)
    }

    // 将所有的配置数据进行转换(如果需要)
    convertErrList := ConvertMgr.CallAll()
    if len(convertErrList) > 0 {
        totalErrList = append(totalErrList, convertErrList...)
    }

    // 将验证后的数据应用到正式数据中
    applyErrList := ApplyMgr.CallAll()
    if len(applyErrList) > 0 {
        totalErrList = append(totalErrList, applyErrList...)
    }

    // 打印数据
    printErrList := PrintMgr.CallAll()
    if len(printErrList) > 0 {
        totalErrList = append(totalErrList, printErrList...)
    }

    // 清理数据(临时变量)
    cleanErrList := CleanMgr.CallAll()
    if len(cleanErrList) > 0 {
        totalErrList = append(totalErrList, cleanErrList...)
    }

    // 设置环境
    setupErrList := SetupMgr.CallAll()
    if len(setupErrList) > 0 {
        totalErrList = append(totalErrList, setupErrList...)
    }

    // 初始化完成
    completeErrList := CompleteMgr.CallAll()
    if len(completeErrList) > 0 {
        totalErrList = append(totalErrList, completeErrList...)
    }

    // 检查全部的错误信息
    if len(totalErrList) > 0 {
        fmt.Println("-------------------------------------------------------------------------------------")
        fmt.Printf("There are %d error(s):\n", len(totalErrList))
        for i, v := range totalErrList {
            fmt.Printf("No.%d: %v\n", i+1, v)
        }
        panic("Please fix them first.")
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354