扫码订阅《 》或入驻星球,即可阅读文章!

GOLANG ROADMAP

阅读模式

  • 沉浸
  • 自动
  • 日常
首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
author-avatar

GOLANG ROADMAP


首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
  • Iris框架中文文档

    • 概要
    • 功能列表
    • 安装
    • HTTP 主机配置
    • 配置信息
    • HTTP 路由
    • Context机制
    • 动态路由参数
    • 路由命名
    • 路由中间件
    • 打包Router
    • 错误处理
    • MVC 架构
    • MVC电影项目示例
    • MVC之Websockets
    • MVC中使用会话
    • 单一控制器
    • 视图(模板引擎)
    • 会话Sessions
    • Websocket

扫码订阅《 》或入驻星球,即可阅读文章!

MVC电影项目示例


GOLANG ROADMAP

# MVC 电影项目示例

Iris 有一个非常强大和炽热 快速 (opens new window) 的 MVC 支持,你可以从一个方法函数里返回你想要的任何类型的值,并且这个值会发送到你预期的客户端。

  • 如果是 string 类型 那么它就是响应的 body。
  • 如果 string 是第二个输出参数,那么它就是内容类型
  • 如果是 int ,那么它就是返回的状态码。
  • 如果 error 并且不为 nil 那么(任何类型)响应将被省略,并且将呈现带有 400 错误请求的错误文本。
  • 如果 (int,error) 并且 error 不是nil,那么响应结果将是错误的文本,状态代码是上面提到的 int 的状态码。
  • 如果是 custom struct 或者 interface{} 或者是 slice 或者是 map ,那么返回的响应除非跟随string内容类型,否则它将呈现为json。
  • 如果是 mvc.Result ,然后执行它的 Dispatch 函数,这样就可以使用好的设计模式在需要的地方拆分模型的逻辑。

没有什么能阻止你使用自己喜欢的文件夹结构。 Iris 是一个低阶 Web 框架,它有一流的MVC 支持,但它不限制你的文件夹结构,这是你的选择。

结构取决于你自己的需求。 我们无法告诉你如何设计自己的应用程序,但你可以仔细查看下面的一个用例示例;

# 数据模型层

接下来开始写数据模型层 Movie 。

// 文件: datamodels/movie.go

package datamodels

// Movie 是基本数据的的结构体.
// 请注意公共标签(适用于我们的 web 应用)
// 应保存在 「web / viewmodels / movie.go」等其他文件中
// 它可以通过嵌入数据模型进行换行。
//电影或声明新的字段,但我们将使用此数据模型作为我们的应用程序
//中唯一的一个电影模型,为了简单起见。
type Movie struct {
    ID     int64  `json:"id"`
    Name   string `json:"name"`
    Year   int    `json:"year"`
    Genre  string `json:"genre"`
    Poster string `json:"poster"`
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 数据源/数据存储层

之后,我们继续为我们的 Movies 创建一个简单的内存存储。

// 文件: datasource/movies.go

package datasource

import "github.com/kataras/iris/_examples/mvc/overview/datamodels"

// 电影是我们想象中的数据源。
var Movies = map[int64]datamodels.Movie{
    1: {
        ID:     1,
        Name:   "Casablanca",
        Year:   1942,
        Genre:  "Romance",
        Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg",
    },
    2: {
        ID:     2,
        Name:   "Gone with the Wind",
        Year:   1939,
        Genre:  "Romance",
        Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg",
    },
    3: {
        ID:     3,
        Name:   "Citizen Kane",
        Year:   1941,
        Genre:  "Mystery",
        Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg",
    },
    4: {
        ID:     4,
        Name:   "The Wizard of Oz",
        Year:   1939,
        Genre:  "Fantasy",
        Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg",
    },
    5: {
        ID:     5,
        Name:   "North by Northwest",
        Year:   1959,
        Genre:  "Thriller",
        Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg",
    },
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 库

可以直接访问“数据源”并可以直接操作数据的层。

这是可选的 (因为你也可以在 Service 内部使用),不过在展示这个例子的时候我们需要创建一个 Repository ,一个处理“低级”数据,可以直接访问 Movies 数据源的库。保留一个 Repository ,它是一个 interface (接口) 因为可能会不同,取决于您开发的应用程序的状态,也就是说在production开发状态下它会使用某些真正的SQL查询数据或者其他一些您用于查询的数据。

// 文件: repositories/movie_repository.go

package repositories

import (
    "errors"
    "sync"

    "github.com/kataras/iris/_examples/mvc/overview/datamodels"
)

// Query代表一种“访客”和它的查询动作。
type Query func(datamodels.Movie) bool

// MovieRepository会处理一些关于movie实例的基本的操作 。
// 这是一个以测试为目的的接口,即是一个内存中的movie库
// 或是一个连接到数据库的实例。
type MovieRepository interface {
    Exec(query Query, action Query, limit int, mode int) (ok bool)

    Select(query Query) (movie datamodels.Movie, found bool)
    SelectMany(query Query, limit int) (results []datamodels.Movie)

    InsertOrUpdate(movie datamodels.Movie) (updatedMovie datamodels.Movie, err error)
    Delete(query Query, limit int) (deleted bool)
}

// NewMovieRepository返回一个新的基于内存的movie库。
// 库的类型在我们的例子中是唯一的。
func NewMovieRepository(source map[int64]datamodels.Movie) MovieRepository {
    return &movieMemoryRepository{source: source}
}

// movieMemoryRepository就是一个"MovieRepository"
// 它负责存储于内存中的实例数据(map)
type movieMemoryRepository struct {
    source map[int64]datamodels.Movie
    mu     sync.RWMutex
}

const (
    // ReadOnlyMode will RLock(read) the data .
    ReadOnlyMode = iota
    // ReadWriteMode will Lock(read/write) the data.
    ReadWriteMode
)

func (r *movieMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) {
    loops := 0

    if mode == ReadOnlyMode {
        r.mu.RLock()
        defer r.mu.RUnlock()
    } else {
        r.mu.Lock()
        defer r.mu.Unlock()
    }

    for _, movie := range r.source {
        ok = query(movie)
        if ok {
            if action(movie) {
                loops++
                if actionLimit >= loops {
                    break // break
                }
            }
        }
    }

    return
}

// Select方法会收到一个查询方法
// 这个方法给出一个单独的movie实例
// 直到这个功能返回为true时停止迭代。
//
// 它返回最后一次查询成功所找到的结果的值
// 和最后的movie模型
// 以减少caller之间的通信
//
// 这是一个很简单但很聪明的雏形方法
// 我基本在所有会用到的地方使用自从我想到了它
// 也希望你们觉得好用
func (r *movieMemoryRepository) Select(query Query) (movie datamodels.Movie, found bool) {
    found = r.Exec(query, func(m datamodels.Movie) bool {
        movie = m
        return true
    }, 1, ReadOnlyMode)

    // set an empty datamodels.Movie if not found at all.
    if !found {
        movie = datamodels.Movie{}
    }

    return
}

// SelectMany作用相同于Select但是它返回一个切片
// 切片包含一个或多个实例
// 如果传入的参数limit<=0则返回所有
func (r *movieMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.Movie) {
    r.Exec(query, func(m datamodels.Movie) bool {
        results = append(results, m)
        return true
    }, limit, ReadOnlyMode)

    return
}

// InsertOrUpdate添加或者更新一个movie实例到(内存)储存中。
//
// 返回最新操作成功的实例或抛出错误。
func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.Movie, error) {
    id := movie.ID

    if id == 0 { // 创建一个新的操作
        var lastID int64
        // 找到最大的ID,避免重复。
        // 在实际使用时您可以使用第三方库去生成
        // 一个string类型的UUID
        r.mu.RLock()
        for _, item := range r.source {
            if item.ID > lastID {
                lastID = item.ID
            }
        }
        r.mu.RUnlock()

        id = lastID + 1
        movie.ID = id

        // map-specific thing
        r.mu.Lock()
        r.source[id] = movie
        r.mu.Unlock()

        return movie, nil
    }

    // 更新操作是基于movie.ID的,
    // 在例子中我们允许了对poster和genre的更新(如果它们非空)。
    // 当然我们可以只是做单纯的数据替换操作:
    // r.source[id] = movie
    // 并注释掉下面的代码;
    current, exists := r.Select(func(m datamodels.Movie) bool {
        return m.ID == id
    })

    if !exists { // 当ID不存在时抛出一个error
        return datamodels.Movie{}, errors.New("failed to update a nonexistent movie")
    }

    // 或者注释下面这段然后用 r.source[id] = m 做单纯替换
    if movie.Poster != "" {
        current.Poster = movie.Poster
    }

    if movie.Genre != "" {
        current.Genre = movie.Genre
    }

    // map-specific thing
    r.mu.Lock()
    r.source[id] = current
    r.mu.Unlock()

    return movie, nil
}

func (r *movieMemoryRepository) Delete(query Query, limit int) bool {
    return r.Exec(query, func(m datamodels.Movie) bool {
        delete(r.source, m.ID)
        return true
    }, limit, ReadWriteMode)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

# 服务层 Service

Service 可以访问 存储库 和 数据模型层 (如果是简单的应用,也可以访问 数据模型层)的函数的层。它应该包含大部分的逻辑。

我们需要一个服务与存储库在high-level和 存储/检索 Movies中进行通信,这将在下面的 Web控制器上使用。

// file: services/movie_service.go

package services

import (
    "github.com/kataras/iris/_examples/mvc/overview/datamodels"
    "github.com/kataras/iris/_examples/mvc/overview/repositories"
)


// `MovieService` 会处理一些 `movie` 数据模型层的 CRUID 操作
// 这取决于 `movie` 存储库 的一些行为.
//这里将数据源和高级组件进行解耦
// 所以,我们可以在不做任何修改的情况下,轻松的切换使用不同的储库类型
// 这个是一个通用的接口
//因为我们可能需要在不的地方修改和尝试不同的逻辑
type MovieService interface {
    GetAll() []datamodels.Movie
    GetByID(id int64) (datamodels.Movie, bool)
    DeleteByID(id int64) bool
    UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error)
}

// NewMovieService 返回默认的 movie 服务层.
func NewMovieService(repo repositories.MovieRepository) MovieService {
    return &movieService{
        repo: repo,
    }
}

type movieService struct {
    repo repositories.MovieRepository
}

// GetAll 返回所有的 movies.
func (s *movieService) GetAll() []datamodels.Movie {
    return s.repo.SelectMany(func(_ datamodels.Movie) bool {
        return true
    }, -1)
}

// GetByID 根据 id 返回一个 movie .
func (s *movieService) GetByID(id int64) (datamodels.Movie, bool) {
    return s.repo.Select(func(m datamodels.Movie) bool {
        return m.ID == id
    })
}

// UpdatePosterAndGenreByID 更新 一个 movie 的 poster 和 genre 字段.
func (s *movieService) UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) {
    // update the movie and return it.
    return s.repo.InsertOrUpdate(datamodels.Movie{
        ID:     id,
        Poster: poster,
        Genre:  genre,
    })
}

// DeleteByID 根据 id 删除一个 movie
//
// Returns true if deleted otherwise false.
func (s *movieService) DeleteByID(id int64) bool {
    return s.repo.Delete(func(m datamodels.Movie) bool {
        return m.ID == id
    }, 1)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

# 视图模型

视图模型,用于在客户端展现的结构。

例子:

import (
    "github.com/kataras/iris/_examples/mvc/overview/datamodels"

    "github.com/kataras/iris/context"
)

type Movie struct {
    datamodels.Movie
}

func (m Movie) IsValid() bool {
    /* do some checks and return true if it's valid... */
    return m.ID > 0
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Iris 可以将任意的自定义数据结构转换成 HTTP 响应, 因此理论上, 下面这样的写法也是被允许的。

// 快速的完成 `kataras/iris/mvc#Result` 接口。
// 发送一个 `Movie`作为一个受限的HTTP响应。
//如果它的 ID 小于等于 0 ,会返回未找到的 404 错误,
// 否则会返回一个正常响应的 JSON 数据响应,
//(控制器默认情况下的返回数据类型)。
//
// 应用程序的逻辑不应该写在这里。
// 这只是响应前的一个验证步骤,
// 这里可以添加一些简单的检查。
//
//这只是一个案例,
// 当设计更大的应用程序时候,想象一下这个特性的潜力。
//
// 从这个控制器的方法函数返回值是 `Movie` 的类型。
// 例如 `controllers/movie_controller.go#GetBy`。
func (m Movie) Dispatch(ctx context.Context) {
    if !m.IsValid() {
        ctx.NotFound()
        return
    }
    ctx.JSON(m, context.JSON{Indent: " "})
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

然而,我们将使用 "数据模型" 作为唯一的模型包 ,因为 Movie 的结构不包含任何的敏感数据。客户端可以看到时它所有的字段,并且不需要任何额外的功能或者验证。

# 控制器

处理 WEB 请求,是服务端和客户端之间的关联所在。

最重要的是 , Controller 是 Iris 框架的入口,它与 MovieService之间进行通信。 我们通常将所有的与 HTTP 相关的内容存放在 web 名字的文件夹中,所以所有控制器都可以放在 web/controllers 目录下。当然也可以根据你个人的喜好 ,使用其他的设计模式。

// file: web/controllers/movie_controller.go

package controllers

import (
    "errors"

    "github.com/kataras/iris/_examples/mvc/overview/datamodels"
    "github.com/kataras/iris/_examples/mvc/overview/services"

    "github.com/kataras/iris"
)

// MovieController is our /movies controller.
type MovieController struct {
    // Our MovieService, it's an interface which
    // is binded from the main application.
    Service services.MovieService
}

// Get 返回 movies 的列表 
// 演示:
// curl -i http://localhost:8080/movies
//
// The correct way if you have sensitive data:
// func (c *MovieController) Get() (results []viewmodels.Movie) {
//     data := c.Service.GetAll()
//
//     for _, movie := range data {
//         results = append(results, viewmodels.Movie{movie})
//     }
//     return
// }
// 否则,只返回数据模型。
func (c *MovieController) Get() (results []datamodels.Movie) {
    return c.Service.GetAll()
}

// GetBy 返回一个 movie
// 演示:
// curl -i http://localhost:8080/movies/1
func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) {
    return c.Service.GetByID(id) // it will throw 404 if not found.
}

// PutBy 更新一个movie
// 演示:
// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1
func (c *MovieController) PutBy(ctx iris.Context, id int64) (datamodels.Movie, error) {
    // 获取请求内的 poster 和 genre 字段数据
    file, info, err := ctx.FormFile("poster")
    if err != nil {
        return datamodels.Movie{}, errors.New("failed due form file 'poster' missing")
    }
    // 关闭
    file.Close()

    // 假设这是一个上传文件的 url ...
    poster := info.Filename
    genre := ctx.FormValue("genre")

    return c.Service.UpdatePosterAndGenreByID(id, poster, genre)
}

// DeleteBy 删除一个 movie
// 演示:
// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1
func (c *MovieController) DeleteBy(id int64) interface{} {
    wasDel := c.Service.DeleteByID(id)
    if wasDel {
        // 返回被删除的 movie 的 id
        return iris.Map{"deleted": id}
    }
    //在这里,我们可以看到一个方法函数可以返回两种类型中的任何一种(map 或者 int),
    // 我们不用指定特定的返回类型。
    return iris.StatusBadRequest
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

web/middleware 里面的最常用的一个中间件例子。

// file: web/middleware/basicauth.go

package middleware

import "github.com/kataras/iris/middleware/basicauth"

// BasicAuth middleware sample.
var BasicAuth = basicauth.New(basicauth.Config{
    Users: map[string]string{
        "admin": "password",
    },
})

1
2
3
4
5
6
7
8
9
10
11
12
13

最后到 main.go.

// file: main.go

package main

import (
    "github.com/kataras/iris/_examples/mvc/overview/datasource"
    "github.com/kataras/iris/_examples/mvc/overview/repositories"
    "github.com/kataras/iris/_examples/mvc/overview/services"
    "github.com/kataras/iris/_examples/mvc/overview/web/controllers"
    "github.com/kataras/iris/_examples/mvc/overview/web/middleware"

    "github.com/kataras/iris"
    "github.com/kataras/iris/mvc"
)

func main() {
    app := iris.New()
    app.Logger().SetLevel("debug")

    // 加载视图模板地址
    app.RegisterView(iris.HTML("./web/views", ".html"))

    // 注册控制器
    // mvc.New(app.Party("/movies")).Handle(new(controllers.MovieController))
    //你也可以使用  `mvc.Configure` 方法拆分编写 MVC 应用程序的配置。
    // 如下所示:
    mvc.Configure(app.Party("/movies"), movies)

    // http://localhost:8080/movies
    // http://localhost:8080/movies/1
    app.Run(
        // Start the web server at localhost:8080
        iris.Addr("localhost:8080"),
        // skip err server closed when CTRL/CMD+C pressed:
        iris.WithoutServerError(iris.ErrServerClosed),
        // enables faster json serialization and more:
        iris.WithOptimizations,
    )
}

// 注意 mvc.Application, 不是 iris.Application.
func movies(app *mvc.Application) {
    // Add the basic authentication(admin:password) middleware
    // for the /movies based requests.
    app.Router.Use(middleware.BasicAuth)

    // 使用数据源中的一些(内存)数据创建 movie 的数据库。
    repo := repositories.NewMovieRepository(datasource.Movies)
    // 创建 movie 的服务,我们将它绑定到 movie 应用程序。
    movieService := services.NewMovieService(repo)
    app.Register(movieService)

    //初始化控制器
    // 注意,你可以初始化多个控制器
    // 你也可以 使用 `movies.Party(relativePath)` 或者 `movies.Clone(app.Party(...))` 创建子应用。
   
    app.Handle(new(controllers.MovieController))
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  • MVC 电影项目示例
  • 数据模型层
  • 数据源/数据存储层
  • 库
  • 服务层 Service
  • 视图模型
  • 控制器