其他
来自社区的 Go-Spring 入门篇
# 拉取 go spring
$ go get github.com/go-spring/go-spring
# 如果需要使用 go-spring 做 web 服务需要
$ go get github.com/go-spring/go-spring-web
$ 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
spring-core 是用于 IoC 容器注入的核心库。 spring-boot 是使用了 spring-core 构建的配置自动载入,还有注入的对象的启动和关闭的统一管理。
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)).Init(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
[INFO] spring-boot/spring-boot-app.go:105 spring boot started
[INFO] spring-gin/spring-gin.go:76 ⇨ http server started on:8080
▌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)).Init(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/")
}
▌controllers/upload/upload.go
package upload
import (
// ……
)
type Controller struct{}
func init() {
SpringBoot.RegisterBean(new(Controller))Init(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 {
// ……
}
$ 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":"该文件已存在"}
▌services/file/file.go
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
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
}
// ……
}
$ 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/application.toml
[spring.application]
name = "demo-config"
[file]
dir = "temp"
▌controllers/upload/upload.go
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)
// ……
}
▌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
type Controller struct {
File types.FileProvider `autowire:""`
Dir string `value:"${file.dir}"`
}
▌services/file/file.go
type Service struct{}
func init() {
var s = new(Service)
// 需要手动声明支持的接口对象,说起来我记得 new(types.FileProvider) 也是能用的
SpringBoot.RegisterBean(s).AsInterface((*types.FileProvider)(nil))
}
▌docker-compose
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]
enable = true
host = "127.0.0.1"
port = 9000
access = "minio"
secret = "minio123"
secure = false
bucket = "demo"
▌modules/minio/minio.go
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,
)
}
▌services/file/file.go
func init() {
SpringBoot.RegisterBean(new(Service)).AsInterface((*types.FileProvider)(nil)).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)).AsInterface((*types.FileProvider)(nil)).ConditionOnBean("minioClient")
}
func (s *Service) PutObject(name string, r io.Reader, size int64) error {
// ……
}
func (s *Service) ExistsObject(name string) bool {
// ……
}
修改 config/application.toml 的 minio.enable 可以切换存储能力。
▌10.1 uber-go/dig
func main() {
c := dig.New()
// 使用 Provide 注入新的对象,可选 group, name 来使用,当然 Provide 的函数变量也是注入的对象
c.Provide(func (file types.FileProvider) (*upload.Controller, error){
return &upload.Controller{
File: file,
}
})
c.Invoke(func (controller *upload.Controller) {
// route init
})
}
▌10.2 defval/inject | goava/di
func main() {
c := di.New(
// 使用 Provide 注入新的对象,可选 name 来使用,当然 Provide 的函数变量也是注入的对象
di.Provide(func (file types.FileProvider) (*upload.Controller, error){
return &upload.Controller{
File: file,
}
})
)
}
接口别名支持 结束清理支持 多接口聚合处理
type Controller struct {
user types.UserService
cfg *config.Config
friend types.FriendService
}
func NewController(cfg *config.Config, user types.UserService, friend types.FriendService) *Controller {
return &Controller{
user: user,
cfg: cfg,
friend: friend,
}
}
▌10.3 几个 di 库的对比表
--------- PUHUI TECH ---------
大家好,我叫林建辉,在之前的公司有起一个花名:`残影`,网络上使用的英文名都是:`zeromake`,如果有看到这个名字那多半是我,在我的 [github](https://github.com/zeromake) 上折腾了不少东西有兴趣可以去看看。