• GO --微服务框架(二) goa


    之前用过go语言的反射来做一些代码生成,参考这篇

    但是这种方式,入侵太强,需要执行对应的申明调用, 所以对GOA框架的自动生成非常感兴趣,于是仔细研究了一下,发现用的比较巧妙, 这里先卖个关子,先看看生成的代码目录结构。

    这里使用adder的desgin文件来生成:

    复制代码
    package design
    
    import (
            . "github.com/goadesign/goa/design"
            . "github.com/goadesign/goa/design/apidsl"
    )
    
    var _ = API("adder", func() {
            Title("The adder API")
            Description("A teaser for goa")
            Host("localhost:8080")
            Scheme("http")
    })
    
    var _ = Resource("operands", func() {
            Action("add", func() {
                    Routing(GET("add/:left/:right"))
                    Description("add returns the sum of the left and right parameters in the response body")
                    Params(func() {
                            Param("left", Integer, "Left operand")
                            Param("right", Integer, "Right operand")
                    })
                    Response(OK, "text/plain")
            })
    
    })
    复制代码

    然后生成对应的目录结构如下(如果不知道怎么生成,参考第一篇):

    复制代码
    qpzhang@qpzhang:~/gocode/src/goa-adder $tree
    .
    ├── app
    │   ├── contexts.go
    │   ├── controllers.go
    │   ├── hrefs.go
    │   ├── media_types.go
    │   ├── test
    │   │   └── operands.go
    │   └── user_types.go
    ├── client
    │   ├── adder-cli
    │   │   ├── commands.go
    │   │   └── main.go
    │   ├── client.go
    │   ├── datatypes.go
    │   └── operands.go
    ├── design
    │   └── design.go
    ├── main.go
    ├── operands.go
    └── swagger
        ├── swagger.json
        └── swagger.yaml
    复制代码
    • APP目录,生成的框架相关代码,包含HTTP的路由
    • client目录,生成是go原生请求server的client测试程序,方便测试
    • swagger目录, 生成的swagger文件,可以用swagger来进行API的描述,这样不用自己写API接口文档了(cool)
    • 然后是main.go  , 程序的主入口
    • operands.go 业务逻辑代码,你需要在这里进行修改
    复制代码
    //operands.go
    
    package main
    
    import (
        "github.com/goadesign/goa"
        "goa-adder/app"
    )
    
    // OperandsController implements the operands resource.
    type OperandsController struct {
        *goa.Controller
    }
    
    // NewOperandsController creates a operands controller.
    func NewOperandsController(service *goa.Service) *OperandsController {
        return &OperandsController{Controller: service.NewController("OperandsController")}
    }
    
    // Add runs the add action.
    func (c *OperandsController) Add(ctx *app.AddOperandsContext) error {
        // TBD: implement   在这里写对应的函数逻辑
        return nil
    }
    复制代码

    非常棒,不用再重复写框架低层那些代码了(路由、编解码等等)。

    虽然之前也用过前公司的框架(那个是利用java的反射自动生成代码),但遇到自动生成代码这事儿,还是止不住兴奋。

    这里先不研究生成的框架代码,先研究一下利用go语言是如何自动生成的吧。

    一般自动生成可以分三个步骤:

    1)通过自描述语言来定义服务和接口(IDL,DSL都OK)

    2)解析描述语言,获取元数据(服务名称,接口名称,接口参数神马的)

    3)根据元数据,以及框架对应的模板,生成重复的代码部分

    我们来看GOA怎么做的,在goagen中加上 --debug 选项,可以保留中间文件。

    复制代码
    //使用命令
    goagen --debug bootstrap -d goa-adder/design
    
    //生成目录
    qpzhang@qpzhang:~/gocode/src/goa-adder $tree -L 1
    .
    ├── app
    ├── client
    ├── design
    ├── goagen009966755
    ├── goagen174102868
    ├── goagen511141286
    ├── goagen585483469
    ├── main.go
    ├── operands.go
    └── swagger
    复制代码
    复制代码
    ├── goagen009966755
    │   ├── goagen
    │   └── main.go
    ├── goagen174102868
    │   ├── goagen
    │   └── main.go
    ├── goagen511141286
    │   ├── goagen
    │   └── main.go
    ├── goagen585483469
    │   ├── goagen
    │   └── main.go
    复制代码

    我们看到,多出几个目录来,而且每个目录,都包含一个main.go和生成的可执行程序,我们随便进入一个目录看看:

    复制代码
    //************************************************************************//
    // Code Generator
    //
    // Generated with goagen v0.0.1, command line:
    // $ goagen
    // --debug bootstrap -d goa-adder/design
    //
    // The content of this file is auto-generated, DO NOT MODIFY
    //************************************************************************//
    
    package main
    
    import (
        "github.com/goadesign/goa/goagen/gen_main"
        "fmt"
        "strings"
        "github.com/goadesign/goa/dslengine"
        _ "goa-adder/design"
    )
    
    
    func main() {
        // Check if there were errors while running the first DSL pass
        dslengine.FailOnError(dslengine.Errors)
    
        // Now run the secondary DSLs
        dslengine.FailOnError(dslengine.Run())
    
        files, err := genmain.Generate()
        dslengine.FailOnError(err)
    
        // We're done
        fmt.Println(strings.Join(files, "
    "))
    }
    复制代码

    然后看出一些端倪,它先把我们design目录整个包含进来,然后调用引擎里面的函数,进行代码的生成。

    这里再回到我们的DSL语言写的文件 design.go

    复制代码
    package design
    
    import (
            . "github.com/goadesign/goa/design"
            . "github.com/goadesign/goa/design/apidsl"
    )
    
    var _ = API("adder", func() {
            Title("The adder API")
            Description("A teaser for goa")
            Host("localhost:8080")
            Scheme("http")
    })
    复制代码

    这里的API,其实就是在调用引擎里预先定义好的函数,在那里定义的呢?看源码

    复制代码
    func API(name string, dsl func()) *design.APIDefinition {
        if design.Design.Name != "" {
            dslengine.ReportError("multiple API definitions, only one is allowed")
            return nil
        }
        if !dslengine.IsTopLevelDefinition() {
            dslengine.IncompatibleDSL()
            return nil
        }
    
        if name == "" {
            dslengine.ReportError("API name cannot be empty")
        }
        design.Design.Name = name
        design.Design.DSLFunc = dsl
        return design.Design
    }
    复制代码

    API函数的调用,生成了对应的Design实例,然后把元数据(这里是Name 和一个匿名函数) 都保存到内存里面了。

    design对象是在程序初始化的时候(源码这里)就定义好了,并把实例注册到生成引擎中去(其实就是把对象实例传过去,方便后续调用)。

    后面调用Generate函数来进行代码的自动生成。

    大概就是这个意思,确实很巧妙,DSL定义的都是匿名全局变量,全局变量又是对已经定义好的元数据函数的调用(例如:API等),然后通过包引用把DSL文件包含进来,这样元数据都存在对应的实例内存中去了。

    然后就可以随便怎么玩了,通过元数据的类型,来生成对应的文件,妙哉!

    但是由于要支持各种嵌套、不同类型以及容错等等,所以实现写起来的代码非常多。

    不过,我们可以按照这个思路,来实现一个简单的例子:

    复制代码
    //main.go
    
    package main
    
    import "fmt"
    
    //定义DSL语言描述的结构体,用于保存DSL里面的数据
    type APIDefinition struct {
        // Name of API
        Name string
        // Title of API
        Title string
        // Description of API
        Desc string
    
        // DSLFunc contains the DSL used to create this definition if any
        DSLFunc func()
    }
    
    //实现DSL对应的API,用于实例化
    func API(name string, dsl func()) *APIDefinition {
        api := new(APIDefinition)
        api.Name = name
        api.DSLFunc = dsl
    
        //偷偷赋值
        g_api = api
    
        return api
    }
    
    //对应的Title赋值
    func Title(val string) {
        if g_api != nil {
            g_api.Title = val
        }
    }
    
    func Description(d string) {
        if g_api != nil {
            g_api.Desc = d
        }
    }
    
    //当前design的实例,这里用全局变量示意
    var g_api *APIDefinition
    
    //根据内存中的存储数据来进行代码生成
    func generateTest() {
        //这里需要执行一下对应的DSLFunc
        g_api.DSLFunc()
        fmt.Println("get Name: ", g_api.Name)
        fmt.Println("get Title: ", g_api.Title)
        fmt.Println("get Desc: ", g_api.Desc)
    }
    
    //这里是DSL申明
    var _ = API("adder", func() {
        Title("The adder API")
        Description("A teaser for goa")
    })
    
    func main() {
        generateTest()
    }
    复制代码

    最后运行一下执行的结果:

    qpzhang@qpzhang:~/gocode/auto-gen $go run main.go
    get Name:  adder
    get Title:  The adder API
    get Desc:  A teaser for goa

    我们已经拿到用户在DSL里面定义的数据了(当然,这里DSL描述是直接写到同一个文件里面,省去了合并引入的过程)。

    OK,代码的自动生成原理已经知道了,后面就要分析框架整体的架构和代码了。

  • 相关阅读:
    第二章—数据类型字符串str
    第二章—数据类型列表list
    第二章—编码
    第二章——进制
    ConfigParser模块
    描述符__get__,__set__,__delete__
    面向对象 ,特殊成员和魔法方法
    异常处理
    反射
    绑定方法与非绑定方法
  • 原文地址:https://www.cnblogs.com/mafeng/p/6732969.html
Copyright © 2020-2023  润新知