V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
zeromake
V2EX  ›  Go 编程语言

go-spring 使用学习

  •  
  •   zeromake ·
    zeromake · 2019-12-28 12:53:59 +08:00 · 7145 次点击
    这是一个创建于 1827 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    • 最近发现了 go-spring 并且发布了 v1.0.0-beta 版。
    • 看了一下感觉挺不错的,最近离职在家学习就花了一天时间学习这边记录一下

    一、安装

    # 拉取 go spring
    $ go get github.com/go-spring/go-spring@master
    
    # 如果需要使用 go-spring 做 web 服务需要以下包
    # go-spring-boot-starter 是使用 spring-boot 包装的支持 web 以及其它的启动器
    $ go get github.com/go-spring/go-spring-boot-starter@master
    # go-spring-web 则是配合 go-spring-boot-starter 使用的各种 web 框架的封装
    $ go get github.com/go-spring/go-spring-web@master
    

    由于 go-spring 现在还是 beta 版,每天都有可能有一些重要更新建议拉取最新的 master

    不过到了后面 go-spring 正式版也许就不需要直接手动拉取 @master 了,请自行判断。

    二、go-spring 项目包结构介绍

    $ tree . -L 1
    .
    ├── CONTRIBUTING.md
    ├── LICENSE
    ├── README.md
    ├── RunAllTests.sh
    ├── RunCodeCheck.sh
    ├── RunGoDoc.sh
    ├── boot-starter
    ├── go.mod
    ├── go.sum
    ├── package-info.go
    ├── spring-boot
    ├── spring-core
    ├── starter-echo
    ├── starter-gin
    └── starter-web
    
    6 directories, 9 files
    

    其中 starter 本来在 go-spring-boot-starter 仓库里,作者为减少引入包已经把这些 starter 移动到了 go-spring 仓库里。

    starter 部分的暂时无视,这样一看就只剩下 spring-corespring-bootboot-starter

    • spring-core 是用于 IoC 容器注入的核心库。
    • spring-boot 是使用了 spring-core 构建的配置自动载入,还有注入的对象的启动和关闭的统一管理。
    • boot-starter 简单启动和监听信号包装器。

    三、一个简单 gin web 服务

    package main
    
    import (
    	SpringWeb "github.com/go-spring/go-spring-web/spring-web"
    	SpringBoot "github.com/go-spring/go-spring/spring-boot"
    	"net/http"
    
    	_ "github.com/go-spring/go-spring/starter-gin"
    	_ "github.com/go-spring/go-spring/starter-web"
    )
    
    func init() {
    	SpringBoot.RegisterBean(new(Controller)).InitFunc(func(c *Controller) {
    		SpringBoot.GetMapping("/", c.Home)
    	})
    }
    
    type Controller struct{}
    
    func (c *Controller) Home(ctx SpringWeb.WebContext) {
    	ctx.String( http.StatusOK, "OK!")
    }
    
    func main() {
    	SpringBoot.RunApplication("config/")
    }
    
    • 其中 init 方法里我们注册了一个 Controller 的空实例,这个不一定要在 init 中注册,可以在 SpringBoot.RunApplication 调用前的任意地方注册,使用 init 的原因是可以不依赖包内部方法只需要导入即可注入。
    • 然后通过 InitFunc 注册路由,SpringBoot.GetMapping 是统一封装的路由挂载器
    • Home(ctx SpringWeb.WebContext) 里的 SpringWeb.WebContext 则封装了请求响应操作。
    • github.com/go-spring/go-spring/starter-gin 导入替换为 github.com/go-spring/go-spring/starter-echo 可以直接替换为 echo 框架。

    执行该文件会打出大量的注册初始化日志,正式版应该会能够关闭。

    $ go run main.go
    register bean "github.com/go-spring/go-spring/starter-web/WebStarter.WebServerStarter:*WebStarter.WebServerStarter"
    register bean "main/main.Controller:*main.Controller"
    register bean "github.com/go-spring/go-spring/spring-boot/SpringBoot.DefaultApplicationContext:*SpringBoot.DefaultApplicationContext"
    register bean "github.com/go-spring/go-spring/starter-web/WebStarter.WebServerConfig:*WebStarter.WebServerConfig"
    wire bean github.com/go-spring/go-spring/spring-boot/SpringBoot.DefaultApplicationContext:*SpringBoot.DefaultApplicationContext
    success wire bean "github.com/go-spring/go-spring/spring-boot/SpringBoot.DefaultApplicationContext:*SpringBoot.DefaultApplicationContext"
    wire bean github.com/go-spring/go-spring/starter-web/WebStarter.WebServerConfig:*WebStarter.WebServerConfig
    success wire bean "github.com/go-spring/go-spring/starter-web/WebStarter.WebServerConfig:*WebStarter.WebServerConfig"
    wire bean github.com/go-spring/go-spring/starter-web/WebStarter.WebServerStarter:*WebStarter.WebServerStarter
    success wire bean "github.com/go-spring/go-spring/starter-web/WebStarter.WebServerStarter:*WebStarter.WebServerStarter"
    wire bean main/main.Controller:*main.Controller
    success wire bean "main/main.Controller:*main.Controller"
    spring boot started
    ⇨ http server started on :8080
    

    访问 http://127.0.0.1 可以看到上面的代码效果。

    该章节代码见 post-1 分支。

    四、拆分 controller 并自动注册路由

    现代项目都是 controller + service 外加一个实体层,这里我们试着把 controller 拆分出去。

    新建一个 controllers 目录下面创建一个 controllers.go 来导入各个独立的 controller

    controllers/home/home.go

    package home
    
    import (
    	SpringWeb "github.com/go-spring/go-spring-web/spring-web"
    	SpringBoot "github.com/go-spring/go-spring/spring-boot"
    	"net/http"
    )
    
    type Controller struct {}
    
    func init() {
    	SpringBoot.RegisterBean(new(Controller)).InitFunc(func(c *Controller) {
    		SpringBoot.GetMapping("/", c.Home)
    	})
    }
    
    func (c *Controller) Home(ctx SpringWeb.WebContext) {
    	ctx.String( http.StatusOK, "OK!")
    }
    

    controllers/controllers.go

    package controllers
    
    // 导入各个 controller 即可实现路由挂载
    import (
    	_ "github.com/zeromake/spring-web-demo/controllers/home"
    )
    
    

    main.go

    package main
    
    import (
    	_ "github.com/go-spring/go-spring/starter-gin"
    	_ "github.com/go-spring/go-spring/starter-web"
    	SpringBoot "github.com/go-spring/go-spring/spring-boot"
    	_ "github.com/zeromake/spring-web-demo/controllers"
    )
    
    func main() {
    	SpringBoot.RunApplication("config/")
    }
    
    

    重新运行 go run main.go 访问浏览器能获得相同的效果,这样我们就把 controller 拆分出去了。

    该章节代码见 post-2 分支。

    五、构建 service 的自动注入到 controller

    上面说到 controller 的主要的能力为路由注册,参数处理复杂的逻辑应当拆分到 service 当中。

    在我使用 go-spring 之前都是手动的构建一个 map[string]interface{} 然后把 service 按照自定义名字挂进去。

    然后在 controller 构建时从这个 map 中取出并强制转换为 service 类型或者抽象的接口。

    这个方案问题蛮大的,手动的 service 名称容易出错,而且注册和在 controller 注入都是非常麻烦的,而且错误处理也都没做。

    但是这一切有了 go-spring 就不一样了,我只需要在 service 注册,在 controller 里的结构体里声明这个 service 类型实例就可以使用。

    为了不作为一个示例而太简单让学习者觉得没有什么意义,我决定做一个上传的能力,先看未拆分 service 的情况

    controllers/upload/upload.go

    package upload
    
    import (
    	// ……
    )
    
    type Controller struct{}
    
    func init() {
    	SpringBoot.RegisterBean(new(Controller))InitFunc(func(c *Controller) {
    		SpringBoot.GetMapping("/upload", c.Upload)
    	})
    }
    
    func (c *Controller) Upload(ctx SpringWeb.WebContext) {
    	file, err := ctx.FormFile("file")
    	if err != nil {
    		// ……
    		return
    	}
    	w, err := file.Open()
    	if err != nil {
    		// ……
    		return
    	}
    	defer func() {
    		_ = w.Close()
    	}()
    	out := path.Join("temp", file.Filename)
    	if !PathExists(out) {
    		dir := path.Dir(out)
    		if !PathExists(dir) {
    			err = os.MkdirAll(dir, DIR_MARK)
    			if err != nil {
    				// ……
    				return
    			}
    		}
    		dst, err := os.OpenFile(out, FILE_FLAG, FILE_MAEK)
    		if err != nil {
    			// ……
    			return
    		}
    		defer func() {
    			_ = dst.Close()
    		}()
    		_, err = io.Copy(dst, w)
    		if err != nil {
    			// ……
    			return
    		}
    	} else {
    		// ……
    		return
    	}
    	ctx.JSON( http.StatusOK, gin.H{
    		"code":    0,
    		"message": http.StatusText( http.StatusOK),
    		"data": map[string]string{
    			"url": out,
    		},
    	})
    }
    
    func PathExists(path string) bool {
    	// ……
    }
    
    

    运行 go run main.go 然后用 curl 上传测试。

    $ curl -F "file=@./README.md" http://127.0.0.1:8080/upload
    {"code":0,"data":{"url":"temp/README.md"},"message":"OK"}
    # 重复上传会发现文件已存在
    $ curl -F "file=@./README.md" http://127.0.0.1:8080/upload
    {"code":1,"message":"该文件已存在"}
    

    在项目下的 temp 文件夹中能够找到上传后的文件。

    以上能正常运行但是 controller 中包含了大量的逻辑而且均为文件操作 api 耦合性过高。

    我们需要把上面的的文件操作拆分到 service 当中。

    services/file/file.go

    将文件操作逻辑抽取为 PutObject(name string, r io.Reader, size int64) (err error)ExistsObject(name string) bool

    package file
    
    type Service struct{}
    
    func init() {
    	SpringBoot.RegisterBean(new(Service))
    }
    
    func (s *Service) PutObject(name string, r io.Reader, size int64) (err error) {
    	// ……
    }
    
    func (s *Service) ExistsObject(name string) bool {
    	// ……
    }
    
    

    services/services.go

    package services
    
    import (
    	_ "github.com/zeromake/spring-web-demo/services/file"
    )
    
    

    main.go

    增加 services 的导入。

    package main
    
    import (
    	// ……
    	_ "github.com/zeromake/spring-web-demo/services"
    )
    
    func main() {
    	SpringBoot.RunApplication("config/")
    }
    
    

    controllers/upload/upload.go

    Controller 上声明 File 并设置 tag autowire,这样 spring-boot 会自动注入 service 那边注册的实例。

    package upload
    
    import (
    	"github.com/gin-gonic/gin"
    	SpringWeb "github.com/go-spring/go-spring-web/spring-web"
    	SpringBoot "github.com/go-spring/go-spring/spring-boot"
    	"github.com/zeromake/spring-web-demo/services/file"
    	"net/http"
    	"path"
    )
    
    type Controller struct {
    	File *file.Service `autowire:""`
    }
    
    func (c *Controller) Upload(ctx SpringWeb.WebContext) {
    	// ……
    	if !c.File.ExistsObject(out) {
    		err = c.File.PutObject(out, w, f.Size)
    		if err != nil {
    			ctx.JSON( http.StatusInternalServerError, gin.H{
    				"code":    1,
    				"message": "保存失败",
    				"error":   err.Error(),
    			})
    			return
    		}
    	} else {
    		ctx.JSON( http.StatusBadRequest, gin.H{
    			"code":    1,
    			"message": "该文件已存在",
    		})
    		return
    	}
    	// ……
    }
    
    

    重新运行 go run main.go 并测试,功能正常

    $ rm temp/README.md
    $ curl -F "file=@./README.md" http://127.0.0.1:8080/upload
    {"code":0,"data":{"url":"temp/README.md"},"message":"OK"}
    
    $ curl -F "file=@./README.md" http://127.0.0.1:8080/upload
    {"code":1,"message":"该文件已存在"}
    

    未拆分 service 的完整代码在 post-3 拆分了 service 的完整代码在 post-4

    六、spring-boot 加载配置注入对象

    我们启动服务时有传入一个 config/ 这个实际上是配置文件搜索路径。

    SpringBoot.RunApplication("config/")
    

    spring-boot 支持不少格式的配置和命名方式,这些都不介绍了。

    只介绍一下怎么使用这些文件

    config/application.toml

    [spring.application]
    name = "demo-config"
    
    [file]
    dir = "temp"
    

    controllers/upload/upload.gocontroller 使用配置替换硬编码的保存文件夹路径, value:"${file.dir}" 对应配置文件的路径绑定。

    type Controller struct {
    	File *file.Service `autowire:""`
    	Dir string `value:"${file.dir}"`
    }
    
    func (c *Controller) Upload(ctx SpringWeb.WebContext) {
    	// ……
    	// 替换为注入的配置
    	out := path.Join(c.Dir, f.Filename)
    	// ……
    }
    

    当然 spring-boot 也支持对结构体实例化配置数据还有默认值。

    
    type Config struct {
    	Dir string `value:"${file.dir=tmp}"`
    }
    
    type Controller struct {
    	File *file.Service `autowire:""`
    	Config Config
    }
    
    func (c *Controller) Upload(ctx SpringWeb.WebContext) {
    	// ……
    	// 替换为注入的配置
    	out := path.Join(c.Config.Dir, f.Filename)
    	// ……
    }
    

    该章完整代码在 post-5

    七、通过接口类型解除 controller 对 service 的依赖

    以上代码已经很完整了,但是 controller 直接导入 service 造成对逻辑的直接依赖,这样会照成很高的代码耦合,而且导入 service 包也比较麻烦。

    这里我们可以使用 interface 来做到解除依赖,这样不仅解决的导入的问题也能够快速的替换 serivce 的实现。

    types/services.go

    之前抽取的抽象方法派上用处了。

    package types
    
    import (
    	"io"
    )
    
    type FileProvider interface {
    	PutObject(name string, r io.Reader, size int64) error
    	ExistsObject(name string) bool
    }
    

    controllers/upload/upload.go

    然后把 *file.Service 类型替换为 types.FileProvider 即可,spring-boot 会自动匹配接口对应的实例。

    type Controller struct {
    	File types.FileProvider `autowire:""`
    	Dir  string             `value:"${file.dir}"`
    }
    

    该章完整代码在 post-6

    八、通过 Condition 来限制 Bean 的注册来做到不同的 service 切换

    上面我们说到用 interface 结构后是可以替换不同的逻辑实现的,这里我们就来一个对象存储和本地文件存储能力的更换,可以通过配置文件替换文件操作逻辑实现。

    这里使用 minio 作为远端对象存储服务。

    docker-compose 这里我们用 docker 快速创建一个本地的 minio 服务。

    version: "3"
    services:
      minio:
        image: "minio/minio:RELEASE.2019-10-12T01-39-57Z"
        volumes:
          - "./minio:/data"
        ports:
          - "9000:9000"
        environment:
          MINIO_ACCESS_KEY: minio
          MINIO_SECRET_KEY: minio123
        command:
          - "server"
          - "/data"
    

    config/application.toml 添加 minio 配置

    [minio]
    enable = true
    host = "127.0.0.1"
    port = 9000
    access = "minio"
    secret = "minio123"
    secure = false
    bucket = "demo"
    

    modules/minio/minio.go 单独的用 module 来做 minio 的客户端初始化。

    package minio
    
    type MinioConfig struct {
    	Enable bool   `value:"${minio.enable:=true}"`    // 是否启用 HTTP
    	Host   string `value:"${minio.host:=127.0.0.1}"` // HTTP host
    	Port   int    `value:"${minio.port:=9000}"`      // HTTP 端口
    	Access string `value:"${minio.access:=}"`        // Access
    	Secret string `value:"${minio.secret:=}"`        // Secret
    	Secure bool   `value:"${minio.secure:=true}"`    // Secure
    	Bucket string `value:"${minio.bucket:=}"`
    }
    
    func init() {
    	SpringBoot.RegisterNameBeanFn(
            // 给这个实例起个名字
            "minioClient",
            // 自动注入 minio 配置
    		func(config MinioConfig) *minio.Client {
    			// ……
            },
            // 前面的 0 代表参数位置,后面则是配置前缀
            "0:${}",
            // ConditionOnPropertyValue 会检查配置文件来确认是否注册
    	).ConditionOnPropertyValue(
    		"minio.enable",
    		true,
    	)
    }
    

    记得收集导入到 main.go

    services/file/file.go

    本地存储 service 需要在没有注册 minioClient 的情况才注册。

    func init() {
    	SpringBoot.RegisterBean(new(Service)).ConditionOnMissingBean("minioClient")
    }
    

    services/minio/minio.go

    package minio
    
    type Service struct {
        // 自动注入 minio client
    	Client *minio.Client `autowire:""`
    	Bucket string        `value:"${minio.bucket:=}"`
    }
    
    func init() {
        // 在已注册了 minioClient 才注册
    	SpringBoot.RegisterBean(new(Service)).ConditionOnBean("minioClient")
    }
    
    func (s *Service) PutObject(name string, r io.Reader, size int64) error {
    	// ……
    }
    
    func (s *Service) ExistsObject(name string) bool {
    	// ……
    }
    
    

    然后启动 docker-compose up -d minio 启动 minio 服务。

    修改 config/application.tomlminio.enable 可以切换存储能力。

    本章完整代码在 post-7

    求职

    我是 zeromake 现在我离职中。

    希望能够找到一个合适的新工作。

    目标:Golang 开发,厦门优先

    我的在线简历: zeromake 的简历

    顺便推广一下: docker-debug

    版权信息

    本文作者: zeromake

    原文链接: [https://blog.zeromake.com/pages/go-spring-learn]https://blog.zeromake.com/pages/go-spring-learn

    最后更新: 2019-12-22 17:20:57+08:00

    版权声明: 本博客所有文章除特别声明外, 均采用 CC BY-NC-SA 4.0 许可协议. 转载请注明出处!

    35 条回复    2019-12-31 09:27:31 +08:00
    waising
        1
    waising  
       2019-12-28 13:48:37 +08:00
    刚要从 java 到 go。。。又来 spring。。
    zeromake
        2
    zeromake  
    OP
       2019-12-28 13:53:18 +08:00
    @waising #1 那你可以手动的做实例化并且在个个组件间使用,不过也可以用一个更简单的注入工具 https://github.com/uber-go/dig 一共就三个方法。
    container := dig.New()
    zjsxwc
        3
    zjsxwc  
       2019-12-28 13:59:42 +08:00
    把 go 写成 java。。
    我写 go 就简单粗暴了,直接撸 init 函数把依赖注入到全局定义的 var sync.Map 变量里
    zeromake
        4
    zeromake  
    OP
       2019-12-28 14:00:51 +08:00
    @zjsxwc #3 我之前也这么干,然后找到了 `dig` 和 `go-spring`
    ArJun
        5
    ArJun  
       2019-12-28 14:19:51 +08:00   ❤️ 1
    spring 那套太重了不喜欢
    CEBBCAT
        6
    CEBBCAT  
       2019-12-28 14:42:33 +08:00 via Android
    来踩一踩,另外:
    异端!!!
    tairan2006
        7
    tairan2006  
       2019-12-28 14:48:16 +08:00
    你用 Spring 干啥,如果只是为了 DI,Google 有官方的 wire
    zeromake
        8
    zeromake  
    OP
       2019-12-28 15:04:56 +08:00
    @tairan2006 #7
    我去看了一下 wire,感觉有点难用还不如用 dig 的感觉
    mailmac
        9
    mailmac  
       2019-12-28 15:25:13 +08:00 via Android
    楼主去了两家听说技术要求比较高的公司。厦门区域的话,几个大厂投一下看看。小公司薪资都一般
    zeromake
        10
    zeromake  
    OP
       2019-12-28 15:36:19 +08:00
    @mailmac #9
    投过一个亿联,也是薪资一般
    gramyang
        11
    gramyang  
       2019-12-28 15:58:07 +08:00 via Android   ❤️ 1
    。。。感觉更啰嗦了
    zeromake
        12
    zeromake  
    OP
       2019-12-28 15:58:42 +08:00
    @mailmac #9
    还有就是两家里的后面那家稿定现在的 node 后端有点炸,技术要求并不高,面试倒是挺难的,我撸了一整年的 crud。
    我觉得要去稿定要么去前端编辑器那边,要么去稿定的 ai 部门,网站后端部门今年没有一个人晋升职级了,前端编辑器好几个都升了。
    zeromake
        13
    zeromake  
    OP
       2019-12-28 16:05:43 +08:00
    @gramyang #11

    还好吧至少比手动做依赖注入方便的多,也比 dig 的少了很多代码,就是 go-spring 的 SpringBoot 强制管理了配置和服务的启动关闭有点入侵的感觉。
    PiersSoCool
        14
    PiersSoCool  
       2019-12-28 16:30:53 +08:00   ❤️ 1
    我想用 Spring 为啥不用 Java。。。
    zeromake
        15
    zeromake  
    OP
       2019-12-28 16:38:43 +08:00
    @PiersSoCool #14
    你搞反了我是想在 go 里用 spring 的依赖注入能力而已,原来我的 web 项目的 controller 和 service 之间的依赖关系和实例化都很麻烦,用了 dig 稍微好点,但是实例化后的对象的属性设置还是要手动做,用了 go-spring 直接就告别了对实例化后的对象做属性设置。
    zunceng
        16
    zunceng  
       2019-12-28 16:46:47 +08:00
    第一反应是 spring 追杀我到 golang 了

    现在微服务的玩法还没碰到过一个微服务复杂到要用依赖注入来处理 想用依赖注入可以用 https://github.com/uber-go/fx
    zeromake
        17
    zeromake  
    OP
       2019-12-28 16:54:30 +08:00
    @zunceng #16
    在用 go-spring 之前就是用的 fx 的底层 dig 做注入啦,不过因为实例化的逻辑还是得自己写就换 go-spring 了。
    paragon
        18
    paragon  
       2019-12-28 17:02:22 +08:00
    看见 go 都开始 spring 我就放心了~
    tairan2006
        19
    tairan2006  
       2019-12-28 18:47:29 +08:00
    @zeromake 反射对性能有影响啊
    zeromake
        20
    zeromake  
    OP
       2019-12-28 19:13:25 +08:00
    @tairan2006 #19 依赖注入只是启动时的依赖获取吧。
    janxin
        21
    janxin  
       2019-12-28 19:16:27 +08:00   ❤️ 1
    你为什么不直接用 Spring,是什么原因让你这么自虐...
    zeromake
        22
    zeromake  
    OP
       2019-12-28 19:32:02 +08:00
    @janxin #21
    因为我只是拿来做 go 的依赖注入啊,又不是 spring 全套。
    petelin
        23
    petelin  
       2019-12-28 19:54:36 +08:00 via iPhone
    依赖为什么要诸如 每一个都放到自己的包里 然后搞个单例 传来传去不香吗
    manami
        24
    manami  
       2019-12-28 19:58:54 +08:00 via Android   ❤️ 1
    穿着棉袄去洗澡
    zeromake
        25
    zeromake  
    OP
       2019-12-28 20:00:15 +08:00
    @petelin #23 不香……很麻烦
    lenqu
        26
    lenqu  
       2019-12-28 20:13:33 +08:00
    一次只干一件事
    dodo2012
        27
    dodo2012  
       2019-12-28 20:55:08 +08:00   ❤️ 1
    算了,我选择自己写,或者用 gin,这搞的太复杂了又
    TypeErrorNone
        28
    TypeErrorNone  
       2019-12-29 00:57:56 +08:00
    真够费劲的,就你们这些人闲的没事,搞这些花里胡哨的东西,go 追求的就是简洁,别整天生搬硬套
    cnbattle
        29
    cnbattle  
       2019-12-29 07:51:34 +08:00 via Android
    go 不需要依赖注入🌚
    slyang5
        30
    slyang5  
       2019-12-29 12:27:30 +08:00
    那为什么不用 JAVA 啊 。。。。。。
    zeromake
        31
    zeromake  
    OP
       2019-12-29 12:41:20 +08:00
    @slyang5 #30
    @TypeErrorNone #28
    @cnbattle #29

    真的是够了,我这边是已经有了项目而且也都是 go 写的,现在只是使用 go-spring 做 service 和 controller 的依赖注入。

    使用 go-spring 可以简化我的 service, controller 实例化。
    cs419
        32
    cs419  
       2019-12-29 22:00:30 +08:00
    很好奇用了 spring 这名字侵权不
    https://github.com/go-spring 一堆的 spring
    go-spring-boot-demo
    go-spring-web
    go-spring-boot-starter
    go-spring-website
    go-spring-rpc
    go-spring-parent
    go-spring-redis
    go-spring-orm
    Kaiv2
        33
    Kaiv2  
       2019-12-30 08:53:17 +08:00 via Android
    感觉很好啊,java 程序员转 go 更容易上手了
    Edward4074
        34
    Edward4074  
       2019-12-30 18:42:11 +08:00
    厦门能有 20K 的很少吧
    brucewuio
        35
    brucewuio  
       2019-12-31 09:27:31 +08:00
    写 Go 我从来不用第三方的库 直接在 SDK 上撸 酸爽
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2797 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 07:43 · PVG 15:43 · LAX 23:43 · JFK 02:43
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.