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
了,请自行判断。
$ 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-core
和 spring-boot
, boot-starter
。
spring-core
是用于 IoC 容器注入的核心库。spring-boot
是使用了 spring-core
构建的配置自动载入,还有注入的对象的启动和关闭的统一管理。boot-starter
简单启动和监听信号包装器。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
+ 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 分支。
上面说到 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":"该文件已存在"}
我们启动服务时有传入一个 config/
这个实际上是配置文件搜索路径。
SpringBoot.RunApplication("config/")
spring-boot
支持不少格式的配置和命名方式,这些都不介绍了。
只介绍一下怎么使用这些文件
config/application.toml
[spring.application]
name = "demo-config"
[file]
dir = "temp"
controllers/upload/upload.go
在 controller
使用配置替换硬编码的保存文件夹路径, 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
造成对逻辑的直接依赖,这样会照成很高的代码耦合,而且导入 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
上面我们说到用 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.toml
的 minio.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 许可协议. 转载请注明出处!
1
waising 2019-12-28 13:48:37 +08:00
刚要从 java 到 go。。。又来 spring。。
|
2
zeromake OP @waising #1 那你可以手动的做实例化并且在个个组件间使用,不过也可以用一个更简单的注入工具 https://github.com/uber-go/dig 一共就三个方法。
container := dig.New() |
3
zjsxwc 2019-12-28 13:59:42 +08:00
把 go 写成 java。。
我写 go 就简单粗暴了,直接撸 init 函数把依赖注入到全局定义的 var sync.Map 变量里 |
5
ArJun 2019-12-28 14:19:51 +08:00 1
spring 那套太重了不喜欢
|
6
CEBBCAT 2019-12-28 14:42:33 +08:00 via Android
来踩一踩,另外:
异端!!! |
7
tairan2006 2019-12-28 14:48:16 +08:00
你用 Spring 干啥,如果只是为了 DI,Google 有官方的 wire
|
8
zeromake OP @tairan2006 #7
我去看了一下 wire,感觉有点难用还不如用 dig 的感觉 |
9
mailmac 2019-12-28 15:25:13 +08:00 via Android
楼主去了两家听说技术要求比较高的公司。厦门区域的话,几个大厂投一下看看。小公司薪资都一般
|
11
gramyang 2019-12-28 15:58:07 +08:00 via Android 1
。。。感觉更啰嗦了
|
12
zeromake OP @mailmac #9
还有就是两家里的后面那家稿定现在的 node 后端有点炸,技术要求并不高,面试倒是挺难的,我撸了一整年的 crud。 我觉得要去稿定要么去前端编辑器那边,要么去稿定的 ai 部门,网站后端部门今年没有一个人晋升职级了,前端编辑器好几个都升了。 |
13
zeromake OP |
14
PiersSoCool 2019-12-28 16:30:53 +08:00 1
我想用 Spring 为啥不用 Java。。。
|
15
zeromake OP @PiersSoCool #14
你搞反了我是想在 go 里用 spring 的依赖注入能力而已,原来我的 web 项目的 controller 和 service 之间的依赖关系和实例化都很麻烦,用了 dig 稍微好点,但是实例化后的对象的属性设置还是要手动做,用了 go-spring 直接就告别了对实例化后的对象做属性设置。 |
16
zunceng 2019-12-28 16:46:47 +08:00
|
17
zeromake OP @zunceng #16
在用 go-spring 之前就是用的 fx 的底层 dig 做注入啦,不过因为实例化的逻辑还是得自己写就换 go-spring 了。 |
18
paragon 2019-12-28 17:02:22 +08:00
看见 go 都开始 spring 我就放心了~
|
19
tairan2006 2019-12-28 18:47:29 +08:00
@zeromake 反射对性能有影响啊
|
20
zeromake OP @tairan2006 #19 依赖注入只是启动时的依赖获取吧。
|
21
janxin 2019-12-28 19:16:27 +08:00 1
你为什么不直接用 Spring,是什么原因让你这么自虐...
|
23
petelin 2019-12-28 19:54:36 +08:00 via iPhone
依赖为什么要诸如 每一个都放到自己的包里 然后搞个单例 传来传去不香吗
|
24
manami 2019-12-28 19:58:54 +08:00 via Android 1
穿着棉袄去洗澡
|
26
lenqu 2019-12-28 20:13:33 +08:00
一次只干一件事
|
27
dodo2012 2019-12-28 20:55:08 +08:00 1
算了,我选择自己写,或者用 gin,这搞的太复杂了又
|
28
TypeErrorNone 2019-12-29 00:57:56 +08:00
真够费劲的,就你们这些人闲的没事,搞这些花里胡哨的东西,go 追求的就是简洁,别整天生搬硬套
|
29
cnbattle 2019-12-29 07:51:34 +08:00 via Android
go 不需要依赖注入🌚
|
30
slyang5 2019-12-29 12:27:30 +08:00
那为什么不用 JAVA 啊 。。。。。。
|
31
zeromake OP @slyang5 #30
@TypeErrorNone #28 @cnbattle #29 真的是够了,我这边是已经有了项目而且也都是 go 写的,现在只是使用 go-spring 做 service 和 controller 的依赖注入。 使用 go-spring 可以简化我的 service, controller 实例化。 |
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 |
33
Kaiv2 2019-12-30 08:53:17 +08:00 via Android
感觉很好啊,java 程序员转 go 更容易上手了
|
34
Edward4074 2019-12-30 18:42:11 +08:00
厦门能有 20K 的很少吧
|
35
brucewuio 2019-12-31 09:27:31 +08:00
写 Go 我从来不用第三方的库 直接在 SDK 上撸 酸爽
|