常用的6款Go语言Web框架

浅析GO语言中的beego框架
Beego:开源的高性能 Go 语言 Web 框架。
Buffalo:使用 Go 语言快速构建 Web 应用。
Echo:简约的高性能 Go 语言 Web 框架。
Gin:Go 语言编写的 Web 框架,以更好的性能实现类似 Martini 框架的 API。
Iris:全宇宙最快的 Go 语言 Web 框架。完备 MVC 支持,未来尽在掌握。
Revel:Go 语言的高效、全栈 Web 框架。

浅析GO语言中的beego框架

https://twitter.com/ThePracticalDev/status/930878898245722112

如果你只是想写一个自己用的小网站,或许你不需要框架,但如果你是要开发一个投入生产运营的网站,那么你肯定会需要一个框架,而且是需要一个好的 Web 框架。

如果你已经掌握所有必要的知识和经验,你会冒险自己去重新开发所有的功能么?你有时间去找满足生产级别要求的库来用于开发么?另外,你确定这个库可以满足你后续所有的要求?

这些都是促使我们去使用框架的原因,哪怕是那些最牛的开发者也不会一直想要重新造轮子,我们可以站在前人的肩膀上,走得更快更好。

介绍

Go 是一门正在快速增长的编程语言,专为构建简单、快速且可靠的软件而设计。 点击 此处 查看有哪些优秀的公司正在使用 Go 语言来驱动他们的业务。

本文将会提供一切必要的信息来帮助开发人员了解更多关于使用 Go 语言来开发 Web 应用程序的最佳选择。

本文包含了最详尽的框架比较,从流行度、社区支持及内建功能等多个不同角度出发做对比。

Beego开源的高性能 Go 语言 Web 框架。

Buffalo使用 Go 语言快速构建 Web 应用。

Echo简约的高性能 Go 语言 Web 框架。

GinGo 语言编写的 Web 框架,以更好的性能实现类似 Martini 框架的 API。

Iris全宇宙最快的 Go 语言 Web 框架。完备 MVC 支持,未来尽在掌握。

RevelGo 语言的高效、全栈 Web 框架。

流行度

按照流行度排行(根据 GitHub Star 数量)

https://github.com/speedwheel/awesome-go-web-frameworks/blob/master/README.md#popularity

学习曲线

https://github.com/speedwheel/awesome-go-web-frameworks/blob/master/README.md#learning-curve

astaxie 和 kataras 分别为 Beego 和 Iris 做了超棒的工作,希望其他的框架也能迎头赶上,为开发者提供更多的例子。至少对于我来说,如果我要切换到一个新的框架,那些例子就是最丰富的资源,来获取尽可能多的有用信息。一个实例胜千言啊。

核心功能

根据功能支持的多寡排行

https://github.com/speedwheel/awesome-go-web-frameworks/blob/master/README.md#core-features

几个知名的 Go 语言 Web 框架并不是真正意义上的框架,也就是说: EchoGin 和 Buffalo并不是真正意义上的 Web 框架(因为没有完备支持所有功能)但是大部分的 Go 社区认为它们是的,因此这些框架也可以和 IrisBeego 或 Revel 做比较。所以,我们有义务将这几个框架(EchoGin 和 Buffalo)也列在这个表中。

以上所有这些框架,除了 Beego 和 Revel 之外,都可以适配任意 net/http 中间件。其中一部分框架可以轻松地做适配,另外一些可能就需要额外的努力 [即使这里的痛苦不是一定的]。

技术性词汇

路由:命名的路径参数和通配符

可以处理动态的路径。

命名的路径参数例子:

"/user/{username}" 匹配 "/user/me","/user/speedwheel" 等等

上面路径参数 username 的值分别是 "me" 和 "speedwheel"

通配符的例子:

"/user/{path *wildcard}" 匹配
"/user/some/path/here",
"/user/this/is/a/dynamic/multi/level/path" 等等

上面的路径参数 path 对应的分别是 "some/path/here" 和 "this/is/a/dynamic/multi/level/path"

Iris 也支持一个叫 macros 的功能,它可以被表示为 /user/{username:string} 或者 /user/{username:int min(1)}

路由:正则表达式

过滤动态的路径。

例如:

"/user/{id ^[0-9]$}" 能匹配 "/user/42" ,但不会匹配 "/user/somestring"

这里的路径参数 id 的值为 42

路由:分组

通过共用逻辑或中间件来处理有共同前缀的路径组。

例如:

myGroup := Group("/user", userAuthenticationMiddleware)
myGroup.Handle("GET", "/", userHandler)
myGroup.Handle("GET", "/profile", userProfileHandler)
myGroup.Handle("GET", "/signup", getUserSignupForm)
  • /user
  • /user/profile
  • /user/signup

你甚至可以从一个组中创建子分组:

myGroup.Group("/messages", optionalUserMessagesMiddleware)
myGroup.Handle("GET', "/{id}", getMessageByID)
  • /user/messages/{id}

路由:上述所有规则相结合而没有冲突

这是一个高级且有用的的功能,我们许多人都希望路由模块或 Web 框架能支持这点,但目前,在 Go 语言框架方面,只有 Iris 能支持这一功能。

这意味着类似如 /{path *wildcard} , /user/{username} 和 /user/static 以及 /user/{path *wildcard} 等路径都可以在同一个路由中通过静态路径(/user/static)或通配符(/{path *wildcard})来正确匹配。

路由:自定义 HTTP 错误

指可以自行处理请求错误的情况。 Http 的错误状态码均 >=400 ,比如 NotFound 404,请求的资源不存在。

例如:

OnErrorCode(404, myNotFoundHandler)

上述的大多数 Web 框架只支持 404405 及 500 错误状态的处理,但是例如 Iris,Beego 和 Revel 等框架,它们完备支持 HTTP 错误状态码,甚至支持 any error 任意错误。( any error — 任意错误,只有 Iris 能够支持)。

100% 兼容 net/http

这意味著:

  • 这些框架能够让你直接获取 *http.Request 和 http.ResponseWriter 的所有相关信息。
  • 各框架提供各自相应处理 net/http 请求的方法。

中间件生态系统

框架会为你提供一个完整的引擎来定义流程、全局、单个或一组路由,而不需要你自己用不同的中间件来封装每一部分的处理器。框架会提供比如 Use(中间件)、Done(中间件) 等函数。

类 Sinatra 的 API 设计(译者注:Sinatra 是一门基于 Ruby 的领域专属语言

可以在运行时中注入代码来处理特定的 HTTP 方法 (以及路径参数)。

例如:

.Get or GET("/path", gethandler)
.Post or POST("/path", postHandler)
.Put or PUT("/path", putHandler) and etc.

服务器程序:默认启用 HTTPS

框架的服务器支持注册及自动更新 SSL 证书来管理新传入的 SSL/TLS 连接 (https)。 最著名的默认启用 https 的供应商是 letsencrypt

服务器程序:平滑关闭(Gracefully Shutdown)

当按下 CTRL + C 关闭你的终端应用程序时,服务器将等待 (一定的等待时间)其他的连接完成相关任务或触发一个自定义事件来做清理工作(比如:关闭数据库),最后平滑地停止服务。

服务器程序:多重监听

框架的服务器支持自定义的 net.Listener 或可以启动一个有多个 http 服务和地址的 Web 应用。

完全支持 HTTP/2

框架可以很好地支持处理 https 请求的 HTTP/2 协议,并且支持服务器 Push 功能。

子域名

你可以直接在你的 Web 应用中注入子域名的路径。

辅助功能(secondary) 意味着这个功能并不被这个框架原生支持,但是你仍旧可以通过启用多个 http 服务器来实现。这样做的缺点在于:主程序和子域名程序之间并不是连通的,默认情况下,它们不能共享逻辑。

会话(Sessions)

支持 http sessions,且可以在自定义的处理程序中使用 sessions。

  • 有一些 Web 框架支持后台数据库来储存 sessions,以便在服务器重启之后仍旧能获得持久的 sessions。
  • Buffalo 使用 gorilla 的 sessions 库,它比其他框架的实现略微慢了一点。

例如:

func setValue(context http_context){
    s := Sessions.New(http_context)
    s.Set("key", "my value")
}

func getValue(context http_context){
    s := Sessions.New(http_context)
    myValue := s.Get("key")
}

func logoutHandler(context http_context){
    Sessions.Destroy(http_context)
}

Wiki: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#HTTP_session

网络套接字(Websockets)

框架支持 websocket 通信协议。不同的框架对于这点有各自不同的实现方式。

你应该通过它们的例子来看看哪个适合你。我的一个同事,在试过了上述所有框架中的 websocket 功能之后告诉我:Iris 实现了最多的 websocket 特性,并且提供了相对更容易使用的 API 。

Wiki: https://en.wikipedia.org/wiki/WebSocket

程序内嵌对视图(又名模版)的支持

通常情况下,你必须根据 Web 应用的可执行文件一一对应地转换模版文件。内嵌到应用中意味着这个框架集成了 go-bindata ,因此在最终的可执行文件中可以以 []byte 的形式将模版包含进来。

什么是视图引擎

框架支持模版加载、自定义及内建模版功能,以此来节省我们的开发时间。

视图引擎:STD

框架支持标准的 html/template 解析器加载模版。

视图引擎:Pug

框架支持 Pug 解析器加载模版。

视图引擎:Django

框架支持 Django 解析器加载模版。

视图引擎:Handlebars

框架支持 Handlebars 解析器加载模版。

视图引擎:Amber

框架支持 Amber 解析器加载模版。

渲染:Markdown, JSON, JSONP, XML…

框架提供一个简单的方法来发送和自定义各种内容类型的响应。

MVC

Model–view–controller (MVC) 模型是一种用于在计算机上实现用户界面的软件架构模式,它将一个应用程序分为互相关联的三部分。这样做的目的是为了:将信息的内部处理逻辑、信息呈现给用户以及从用户获取信息三者分离。MVC 设计模式将这三个组件解耦合,从而实现高效的代码复用和并行开发。

  • Iris 支持完备的 MVC 功能, 可以在运行时中注入。
  • Beego 仅支持方法和数据模型的匹配,可以在运行时中注入。
  • Revel 支持方法,路径和数据模型的匹配,只可以通过生成器注入(生成器是另外一个不同的软件用于构建你的 Web 应用)。

Wiki: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller

缓存

Web 缓存(或 http 缓存)是一种用于临时存储(缓存)网页文档,如 HTML 页面和图像,来减缓服务器延时。一个 Web 缓存系统缓存网页文档,使得后续的请求如果满足特定条件就可以直接得到缓存的文档。Web 缓存系统既可以指设备,也可以指软件程序。

Wiki: https://en.wikipedia.org/wiki/Web_cache

文件服务器

可以注册一个(物理的)目录到一个路径,使得这个路径下的文件可以自动地提供给客户端。

文件服务器:内嵌入应用

通常情况下,你必须将所有的静态文件(比如静态资产,assets:CSS,JavaScript 文件等)与应用程序的可执行文件一起传输。支持此项功能的框架为你提供了在应用中,以 []byte 的形式,内嵌所有这些数据的机会。由于服务器可以直接使用这些数据而无需在物理位置查找文件,它们的响应速度也将更快。

响应可以在发送前的生命周期中被多次修改

目前只有 Iris 通过 http_context 中内建的的响应写入器(response writer)支持这个功能。

当框架支持此功能时,你可以在返回给客户端之前检索、重置或修改状态码、正文(body)及头部(headers)。默认情况下,在基于 net/http 的 Web 框架中这是不可能的,因为正文和状态码一经写定就不能被检索或修改。

Gzip

当你在一个路由的处理程序中,并且你可以改变响应写入器(response writer)来发送一个用 gzip 压缩的响应时,框架会负责响应的头部。如果发生任何错误,框架应该把响应重置为正常,框架也应该能够检查客户端是否支持 gzip 压缩。

gzip 是用于压缩和解压缩的文件格式和软件程序。

Wiki: https://en.wikipedia.org/wiki/Gzip

测试框架

可以使用框架特定的库,来帮助你轻松地编写更好的测试代码来测试你的 HTTP 。

例如(目前仅 Iris 支持此功能):

func TestAPI(t *testing.T) {
    app := myIrisApp() 
    tt := httptest.New(t, app)
    tt.GET("/admin").WithBasicAuth("name", "pass").Expect().
    Status(httptest.StatusOK).Body().Equal("welcome")
}

myIrisApp 返回你虚构的 Web 应用,它有一个针对 /admin 路径的 GET 方法,它有基本的身份验证逻辑保护。

上面这个简单的测试,用 "name" 和 "pass" 通过身份验证并访问 GET /admin ,检查它的响应状态是否为 Status OK,并且响应的主体是否为 "welcome" 。

TypeScript 转译器

TypeScript 的目标是成为 ES6 的超集。除了标准定义的所有新特性外,它还增加了静态类型系统。TypeScript 还有转换器用于将 TypeScript 代码(即 ES6 + 类型)转换为 ES5 或 ES3 JavaScript 代码,如此我们就可以在现今的浏览器中运行这些代码了。

在线编辑器

在在线编辑器的帮助下,你可以快速轻松地在线编译和运行代码。

日志系统

自定义日志系统通过提供有用的功能,如彩色日志输出、格式化、日志级别分离及不同的日志记录后端等,来扩展原生日志包。

维护和自动更新

以非侵入的方式通知框架的用户即时更新。



浅析GO语言中的beego框架

beego是一个快速开发Go应用的http框架,作者是SegmentFault 用户,go 语言方面技术大牛。beego可以用来快速开发API、Web、后端服务等各种应用,是一个RESTFul的框架,主要设计灵感来源于tornado、sinatra、flask这三个框架,但是结合了Go本身的一些特性(interface、struct继承等)而设计的一个框架。
架构

beego是基于八大独立的模块之上构建的,是一个高度解耦的框架。当初设计beego的时候就是考虑功能模块化,用户即使不适用beego的http逻辑,也是可以在使用这些独立模块,例如你可以使用cache模块来做你的缓存逻辑,使用日志模块来记录你的操作信息,使用config模块来解析你各种格式的文件,所以不仅仅在beego开发中,你的socket游戏开发中也是很有用的模块,这也是beego为什么受欢迎的一个原因。大家如果玩过乐高的话,应该知道很多高级的东西都是一块一块的积木搭建出来的,而设计beego的时候,这些模块就是积木,高级机器人就是beego。至于这些模块的功能以及如何使用会在后面的文档会逐一介绍。
执行逻辑

既然beego是基于这些模块构建的,那么他的执行逻辑是怎么样的呢?beego是一个典型的MVC架构,他的执行逻辑如下图所示:
项目结构

一般的beego项目的目录如下所示:
├── conf
│ └── app.conf
├── controllers│
├── admin
│ └── default.go
├── main.go
├── models
│ └── models.go
├── static│
├── css│
├── ico
│ ├── img
│ └── js└── views
├── admin
└── index.tpl
从上面的目录结构我们可以看出来M(models目录)、V(views目录)、C(controllers目录)的结构,main.go是入口文件。

选择 Go 语言
断断续续看了 Go 几个星期了,讲真的真是喜欢的不得了。认真学过之后,你会觉得非常的优雅,写东西很舒服。学习 Go 我觉得很有必要的是,Go 中自带的数据结构很少,类似于 List 或者 Tree 之类的,最好尝试一下如何去设计一些常用的数据结构。话说回来,Go 的出身始终是一门后端语言。我非常后悔用 Flask 或者 Django 来作为我的后端入门框架或者选择。封装的太好了,往往对于一个入门新手来说学习不到什么。

而 Go 就不一样了,它天生被设计是一门后端语言。也就是说,你将会学习到非常多的后端知识。看看下面这一张图,当时我看着就有一种很过瘾的感觉,因为这些知识你都知道,但是作为一个后端开发者你没有去了解过,这是非常大的失误。并不是想去用学习好 Go 去弥补没有学习好 C++ 的遗憾,只是新生事物,多尝试尝试总是极好的,哪怕只是浅尝辄止。Go 作为一门新的语言,其语言设计的特性,背后 Google 爸爸的撑腰以及现在 Docker 技术发展,前景应该还是不错的。所以如果你是编程新手或者是想入门后端的开发者,我强烈建议你选择 Go 语言。

语言学到最后,框架总是少不了的。虽然不能完全依赖框架,但是还是可以学习一下框架的设计思想。对于 Beego 框架的评价总是各种各样,这也要看自己的选择了。之所以选择 Beego 框架来入门,主要是因为其详细的文档以及教程示例非常多。

Go Web 初探

先看一下最基本的 Go 中的 web 服务,只用到了 Go 中的 net/http 这个包:

package main
 
    import (
        "fmt"
        "net/http"
        "strings"
        "log"
    )
 
    func sayhelloName(w http.ResponseWriter, r *http.Request) {
        r.ParseForm()  //解析参数,默认是不会解析的
        fmt.Println(r.Form)  //这些信息是输出到服务器端的打印信息
        fmt.Println("path", r.URL.Path)
        fmt.Println("scheme", r.URL.Scheme)
        fmt.Println(r.Form["url_long"])
        for k, v := range r.Form {
            fmt.Println("key:", k)
            fmt.Println("val:", strings.Join(v, ""))
        }
        fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的
    }
 
    func main() {
        http.HandleFunc("/", sayhelloName) //设置访问的路由
        err := http.ListenAndServe(":9090", nil) //设置监听的端口
        if err != nil {
            log.Fatal("ListenAndServe: ", err)
        }
    }

安装 Go 以及 Beego

基本的你得有个 Go 语言的环境,安装什么的就不讲了。只是最后配置其环境变量许多人都容易弄错,包括我自己也是。其实多次也能够配置好,只是每次重新启动就提示上次的配置无效,也不知道是怎么回事。讲一下安装 Beego 框架

export GOBIN="/usr/local/go/bin"
export GOPATH="/Users/allenwu/GoProjects"
export PATH="$PATH:$GOBIN:$GOPATH/bin"

在终端输入如上所示代码,其中 allenwu 替换成你自己的 username,并且在根目录下创建 GoProjects 文件夹,作为下一步工作目录。配置好之后,输入如下命令确保保存成功:

source ~/.zshrc

最后当然要测试一下环境变量是否配置成功,在 shell 中输入 go env若如下所示即表明成功(着重注意一下的就是 gopath 是不是为空):

配置环境没问题之后,就是安装 go 和 bee 工具了:

$ go get github.com/astaxie/beego
$ go get github.com/beego/bee

检查安装是否成功,启动一个 Hello world级别的 App:

$ cd $GOPATH/src
$ bee new hello
$ cd hello
$ bee run hello

会提示你在浏览器输入地址,然后就能知道是否安装成功啦。

体验 Beego 框架
如下图所示即为 Beego 官网所提供的 Beego 框架概览,一眼就能明白其 MVC 模式的构造,结构也是非常清晰的。

安装好 Beego 框架之后,官方给了三个 samples,我们选择其中一个来进行入门体验一下。如下实例选择的是 todo App。我们将 clone 下来的 todo App 放置到指定目录下,用 sublimeText3 打开这个示例项目,强烈建议你打开侧边栏设置选项:

了解完基本的结构之后,我们启动这个 App。我们采用 Beego 提供的工具 bee 来启动。进入到最终的指定文件夹 todo 之后,执行 bee run 命令:

可以看到打印出一个 Bee 的 Logo,表示启动成功了,稍等一下就会继续提示你在浏览器中输入指定 IP 地址和端口号,也就是如下所示:

官方讲这个小 App 结合了 Angular ,体验还是挺不错的。接下来我们来简单分析一下示例 App 的代码结构。首先入口程序是 Main.go,这是想都不用想的,一个程序员的直觉:

package main
 
import (
	"github.com/astaxie/beego"
  	// 注释一
	"samples/todo/controllers"
)
 
func main() {
  	// 注释二
	beego.Router("/", &controllers.MainController{})
	beego.Router("/task/", &controllers.TaskController{}, "get:ListTasks;post:NewTask")
	beego.Router("/task/:id:int", &controllers.TaskController{}, "get:GetTask;put:UpdateTask")
  	// 注释三
	beego.Run()
}

我们第一感觉还是看到 Main() 函数,看到了 Router() 函数,有一些 web 开发或者开发经验的应该都知道这是路由机制。对应的是 url 与 Controller。在 MVC 框架中 Controller 是一个很重要的概念了。我们自然下一步骤就是去往 Controller 中看看:

package controllers
 
import (
	"github.com/astaxie/beego"
)
// 注释一
type MainController struct {
	beego.Controller
}
// 注释二
func (this *MainController) Get() {
  // 注释三
	this.TplName = "index.html"
	this.Render()
}

在看完 Go 的基本语法之后,看到注释一应该也能明白一个一二三四了,我们声明了一个控制器 MainController,这个控制器里面内嵌了 beego.Controller,这就是 Go 的嵌入方式,也就是 MainController 自动拥有了所有 beego.Controller 的方法。而 beego.Controller 拥有很多方法,其中包括 Init、Prepare、Post、Get、Delete、Head等 方法。我们可以通过重写的方式来实现这些方法,而我们上面的代码就是重写了 Get 方法。

在注释三处,我们看到了 index.html,应该明白了 Get 方法去获取对应名称的 HTML 文件,并进行渲染。到这里我们很简单的讲述了一下 MVC 中的 V 和 C,发现没,Model 竟然不知道从哪去讲。还请回头看看 Main.go 中的注释二处:

beego.Router("/task/", &controllers.TaskController{}, "get:ListTasks;post:NewTask")

上述路由引导我们进入了 TaskController 这个控制器来了,我们分析一下下面这个文件:

package controllers
 
import (
	"encoding/json"
	"strconv"
 
	"github.com/astaxie/beego"
	"samples/todo/models"
)
 
type TaskController struct {
	beego.Controller
}
 
// Example:
//
//   req: GET /task/
//   res: 200 {"Tasks": [
//          {"ID": 1, "Title": "Learn Go", "Done": false},
//          {"ID": 2, "Title": "Buy bread", "Done": true}
//        ]}
func (this *TaskController) ListTasks() {
	res := struct{ Tasks []*models.Task }{models.DefaultTaskList.All()}
	this.Data["json"] = res
	this.ServeJSON()
}

很明显我们看到了 models 关键字,并且调用了其中的 Task ,我们选择进入 Task.go 文件看看:

package models
 
import (
	"fmt"
)
 
var DefaultTaskList *TaskManager
 
// Task Model
type Task struct {
	ID    int64  // Unique identifier
	Title string // Description
	Done  bool   // Is this task done?
}
 
// NewTask creates a new task given a title, that can't be empty.
func NewTask(title string) (*Task, error) {
	if title == "" {
		return nil, fmt.Errorf("empty title")
	}
	return &Task{0, title, false}, nil
}
 
// TaskManager manages a list of tasks in memory.
// 注释一
type TaskManager struct {
	tasks  []*Task
	lastID int64
}
 
// NewTaskManager returns an empty TaskManager.
func NewTaskManager() *TaskManager {
	return &TaskManager{}
}
 
// Save saves the given Task in the TaskManager.
func (m *TaskManager) Save(task *Task) error {
	if task.ID == 0 {
		m.lastID++
		task.ID = m.lastID
		m.tasks = append(m.tasks, cloneTask(task))
		return nil
	}
 
	for i, t := range m.tasks {
		if t.ID == task.ID {
			m.tasks[i] = cloneTask(task)
			return nil
		}
	}
	return fmt.Errorf("unknown task")
}
 
// cloneTask creates and returns a deep copy of the given Task.
func cloneTask(t *Task) *Task {
	c := *t
	return &c
}
 
// All returns the list of all the Tasks in the TaskManager.
func (m *TaskManager) All() []*Task {
	return m.tasks
}
 
// Find returns the Task with the given id in the TaskManager and a boolean
// indicating if the id was found.
func (m *TaskManager) Find(ID int64) (*Task, bool) {
	for _, t := range m.tasks {
		if t.ID == ID {
			return t, true
		}
	}
	return nil, false
}
 
func init() {
	DefaultTaskList = NewTaskManager()
}

如上所示的 task Model 主要就是定义了 task 这个实体该有的成员变量。以及一个 taskManager 来管理这些 task,其整体结构在理解了 Go 语言的一些基本的机制之后还是比较简单的。

在之前的整个实例结构中,我们还看到了如下所示的静态文件,它们的作用就很明显啦:

├── static
    │   ├── css
    │   ├── img
    │   └── js

好了,以上就是 Go 的一个框架 Beego 的入门实例了,其实很简单。我也只是简单的写一下入门的东西。后续研究一下 Go 的自动化 API 构建。往后继续学习 Go 和 Docker 的结合应用吧。

【参考文章】
1、Go中国技术社区 – golang https://gocn.io/

2、首页 – beego: 简约 & 强大并存的 Go 应用框架 https://beego.me/

作者:

喜欢围棋和编程。

 
发布于 分类 编程标签

发表评论

邮箱地址不会被公开。