前言
在课程进行到第二天的时候我们就接触了Go语言的一款web开发框架Gin
,我也是认认真真把项目写完了才来和大家交流,同时分享一些自己的拓展
自己也是才入门Go语言,讲的不恰当的地方欢迎各位大佬指正,我一定虚心学习
PS:和老师代码有一些不一样,命名不一样,结构有区别,有拓展
项目准备
我们采用自底向上的方式来开发这个项目,步骤如下
1 编写和数据实体对应的结构体(Topic ,Post )
2 编写模拟仓库,模拟数据库
3 编写dao 层,用于和数据库交互
4 编写service 层,用于处理业务逻辑
5 编写Controller 层,用于处理和view的交互
代码编写
大致梳理了一下项目的结构就要开始正式编写代码了
编写结构体
这是我entity文件夹下的文件结构,Go语言推荐使用全小写,下划线分割的方式来命名文件
├─entity
│ my_time.go
│ page_info.go
│ post.go
│ topic.go
首先是Topic结构体
Go
package entity
// 主题结构体
type Topic struct {
Id uint64
Title string
Content string
CreateTime MyTime
}
接下来是POST结构体
Go
package entity
// 主题下面的回复
type Post struct {
Id uint64
ParentId uint64
Content string
CreateTime MyTime
}
细心的小伙伴应该发现了我的CreateTime
字段没有使用time.Time
类型,而是使用了自己写的一个名为MyTime
的类型,那么这里面到底暗藏什么玄只因呢。 先看代码:
Go
package entity
import (
"fmt"
"time"
)
// 自定义时间格式
const myTimeFormat string = "2006-01-02 15:04:05"
type MyTime struct {
time.Time
}
func (t *MyTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, t.Format(myTimeFormat))), nil
}
func (t *MyTime) UnmarshalJSON(b []byte) error {
// 去掉首尾的引号
trimmedBytes := b[1 : len(b)-1]
// 字符串转时间对象
newTime, err := time.Parse(myTimeFormat, string(trimmedBytes))
if err != nil {
return err
}
t.Time = newTime
return nil
}
有聪明的小伙伴看一眼应该就知道了个大概,我是想通过这种方式来控制CreateTime
这个字段的序列化 与反序列化。虽然一眼就能看个大概,但是这里面还是有很多容易被忽略的细节,接下来详细讲讲这些细节。
我的结构体Mytime
里面只有一个time.Time
,这明明是一个类型,为什么当成字段放在那里了?其实这种写法叫做匿名字段 (Anonymous Fields)或嵌入字段 (Embedded Fields)。像我这么写的话,我的结构体MyTime
能调用time.Time
的所有方法了。学过C++,Java的小伙伴肯定会觉得眼熟,这不就是继承嘛。唉,还真有点那个意思,不过这个比继承简单,毕竟Go语言鼓励使用组合来代替继承。
言归正传,我的结构体MyTime
能调用time.Time
的所有方法,自然也能重写它的方法,所以我这里重写了MarshalJSON
和UnmarshalJSON
这两个方法,重写这两个方法能让我以自己的方式来控制如何序列化 和反序列化这个结构体。
在方法的最后几行,有一行t.Time = newTime
代码可能比较迷惑,其实Time
就是匿名字段 ,是可以调用的,把time.Time
以匿名的方式放入结构体MyTime
内部,就产生了一个名为Time
的匿名字段 ,我之前说MyTime
可以调用time.Time
的所有方法,其实就是调用了Time
这个匿名字段的所有方法。
匿名字段 的特殊之处就在于结构体对象可以隐式的调用匿名字段 的方法,看起来这些方法好像本来就属于结构体一样,而不是通过MyTime.Time.xxxMethod
的方式来调用方法,所以这和继承 没有关系,匿名字段 这种特性看起来像继承 ,本质还是对象的组合 。这也体现了Go语言组合优于继承的理念
有一点需要强调的是,尽管MyTime
能调用time.Time
的所有方法,但是这两个是完全不同的类型,不能把MyTime
类型的值直接赋值给time.Time
。
在Marshal
方法里面有一个比较有意思的地方,fmt.Sprintf(`"%s"`, t.Format(myTimeFormat))
,明明t.Format
都已经返回一个字符串了,为什么还要再在外面包裹一层双引号呢?原因其实很简单,t.Format
返回的字符串只是字符串的内容,而不是字符串,又因为JSON 格式要求字符串被双引号包裹起来,所以才会在字符串外面包裹一层双引号,使其符合JSON格式规范。
编写模拟数据库
这是我repository文件夹下的文件目录
├─repository
│ initial_data.json
│ system_repository.go
模拟数据库的代码
Go
package repository
import (
"encoding/json"
"io"
"os"
"github.com/jun-chiang/go-web-demo1/entity"
)
// 为类型定义别名 如果没有 '=' 就是定义新的类型
type Topic = entity.Topic
type Post = entity.Post
// 存储数据的变量
var (
// 定义话题索引
TopicIndexMap map[uint64]*Topic = make(map[uint64]*Topic, 5)
// 定义并初始化话题回复索引
PostIndexMap map[uint64][]*Post = make(map[uint64][]*entity.Post, 5)
)
// 从文件初始话题索引
func InitTopicIndexMap() error {
// 读取json文件
jsonFile, err := os.Open("repository/initial_data.json")
if err != nil {
return err
}
// 延迟关闭文件流
defer jsonFile.Close()
// 读取全部文件
var topics []Topic
byteValue, err := io.ReadAll(jsonFile)
if err != nil {
return err
}
// json反序列化
err = json.Unmarshal(byteValue, &topics)
if err != nil {
return err
}
for i := range topics {
topic := topics[i]
// 以ID为key,把对象指针放到Map里面去
TopicIndexMap[topic.Id] = &topic
}
return nil
}
为了方便查询,我使用两个map
来存储数据,把结构体的id 作为key ,结构体指针作为value,这样就能很快捷地查找我们需要的结构体对象
整个文件就一个函数InitTopicIndexMap
,这个函数负责初始化模拟数据库,所谓的数据库就是我那两个map
,这里只初始化了Topic
,默认所有话题都是新发布的,没有回复。这里面只有简单的文件操作和json 操作。需要注意的是这里的相对路径 是相对项目的目录 来说的,而不是调用时候的函数的代码,原因就是最后运行go build
的时候生成的.exe
文件是默认生成在项目的文件夹下的。
总结
相对于老师的项目,我把CreateTime
字段做了扩展,修改了这个字段的序列化 和反序列化 。匿名字段 虽然不是什么高级的技术,但是对于我这种才接触Go语言的新手来说,这是我在其他语言里面没有接触过的特性,感觉很新奇,我只不过是简单的使用,希望能加深对其的理解,在将来遇到更复杂的需求的时候能把这种特性运用的得心应手。希望你在看完这篇文章之后也能对匿名字段有所理解。
好了,今天的分享就到这里,文章还有后续,我们下期见!