上手Go青训营Web项目快看过来(一) | 青训营

前言

在课程进行到第二天的时候我们就接触了Go语言的一款web开发框架Gin,我也是认认真真把项目写完了才来和大家交流,同时分享一些自己的拓展

自己也是才入门Go语言,讲的不恰当的地方欢迎各位大佬指正,我一定虚心学习

PS:和老师代码有一些不一样,命名不一样,结构有区别,有拓展

项目准备

我们采用自底向上的方式来开发这个项目,步骤如下
1 编写和数据实体对应的结构体(TopicPost
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的所有方法,自然也能重写它的方法,所以我这里重写了MarshalJSONUnmarshalJSON这两个方法,重写这两个方法能让我以自己的方式来控制如何序列化反序列化这个结构体。

在方法的最后几行,有一行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语言的新手来说,这是我在其他语言里面没有接触过的特性,感觉很新奇,我只不过是简单的使用,希望能加深对其的理解,在将来遇到更复杂的需求的时候能把这种特性运用的得心应手。希望你在看完这篇文章之后也能对匿名字段有所理解。

好了,今天的分享就到这里,文章还有后续,我们下期见!

相关推荐
Find25 天前
MaxKB 集成langchain + Vue + PostgreSQL 的 本地大模型+本地知识库 构建私有大模型 | MarsCode AI刷题
青训营笔记
理tan王子25 天前
伴学笔记 AI刷题 14.数组元素之和最小化 | 豆包MarsCode AI刷题
青训营笔记
理tan王子25 天前
伴学笔记 AI刷题 25.DNA序列编辑距离 | 豆包MarsCode AI刷题
青训营笔记
理tan王子25 天前
伴学笔记 AI刷题 9.超市里的货物架调整 | 豆包MarsCode AI刷题
青训营笔记
夭要7夜宵1 个月前
分而治之,主题分片Partition | 豆包MarsCode AI刷题
青训营笔记
三六1 个月前
刷题漫漫路(二)| 豆包MarsCode AI刷题
青训营笔记
tabzzz1 个月前
突破Zustand的局限性:与React ContentAPI搭配使用
前端·青训营笔记
Serendipity5651 个月前
Go 语言入门指南——单元测试 | 豆包MarsCode AI刷题;
青训营笔记
wml1 个月前
前端实践-使用React实现简单代办事项列表 | 豆包MarsCode AI刷题
青训营笔记
用户44710308932421 个月前
详解前端框架中的设计模式 | 豆包MarsCode AI刷题
青训营笔记