Go语言学习(二)

发布于:2024-12-19 ⋅ 阅读:(74) ⋅ 点赞:(0)

一、Go项目结构抽象

├── bin                      # 存放项目过程中生成的可执行文件
├── script                   # 存放项目中需要使用的脚本文件,shell脚本,或者项目使用的功能性脚本
├── document                 # 项目的说明文档
│   ├── README.md
├── schema                   # 自定义的,可以用于存放定义的请求结构体,常量等
│   ├── actions
│   │   ├── actions.go
│   ├── common
│   │   ├── common.go
├── Dockerfile               # docker文件
├── conf_online              # 线上的配置文件,同理还有测试配置,线下配置等
│   ├── port.conf            # 配置端口等
│   ├── auth.toml            # 配置用户认证等SK,AK等内容
│   ├── logit                # 配置日志,如日志文件名,大小,日志等级,日志格式等,可以针对不同的Server配置不同的内容,Redis,MySQL,HTTPServer等各自使用不同的配置文件
│   ├── app.toml             # 此项目的配置,项目名称,端口,发布模式
│   ├── const.toml           # 全局常量配置
│   ├── server.toml          # 读写超时,监听端口
│   ├── servicer             # 针对不同的服务,配置相关内容,如Redis和MySQL连接配置
│   ├── derive               # 驱动配置
│   ├── bos.toml             # 使用BOS存储的相关配置AK,SK,Domain等信息
├── data                     # 项目需要使用的数据存放目录
│   ├── static_dict
│   ├── dynamic_dict
│   ├── var
├── go.mod                   # mod文件
├── go.env                   # Go环境变量
├── vendor                   # 使用的第三方库
├── library                  # 自己写的库文件
│   ├── resource             # 错误类型,返回定义
│   ├── utils                # 其他工具
│   ├── mysql                # 操作MySQL相关
│   ├── bos                  # 操作BOS相关工具
│   ├── redisclient          # 客户端测试相关
│   ├── types                # 类型定义
├── conf                     # 同上
│   ├── port.conf
│   ├── auth.toml
│   ├── logit
│   ├── app.toml
│   ├── const.toml
│   ├── server.toml
│   ├── servicer
│   ├── afsshell.conf
│   ├── bos.toml
├── Makefile
├── main.go                  # 主入口文件
├── go.sum
├── webroot
│   ├── README.md
├── log                      # 项目的日志输出文件,可以按日志输出的服务和类型进行分类
│   ├── panic
│   ├── service
├── servers                  # 服务端
│   ├── adminserver
│   │   ├── admin.go
│   │   ├── controller
│   ├── httpserver
│   │   ├── router.go        # 路由配置,添加对应的controller
│   │   ├── server.go        # 创建一个server
│   │   ├── controller       # 进行参数校验,调用service
│   │   │   ├── hello.go
│   ├── start.go             # 启动服务
├── bootstrap                
│   ├── shutdown.go          
│   ├── bootstrap.go         # 启动前的各种初始化操作,初始化日志,Redis,MySQL
├── model
│   ├── service              # 调用Dao层进行工作
│   ├── dao                  # 和数据库层直接进行操作
├── README.md

路由管理将请求路径映射到指定的控制器的指定方法中,控制器中只做简单的任务,主要还是调用业务层的方法来完成主要逻辑,定义实体时,可一并设置实体属性的赋值方法以及创建实体的方法。如果有一些实体使用的常量可以单独在一个包中定义出来,定义实体之后,就需要定义和实体相关的方法并放入到一个接口中,该接口所拥有的方法在具体的业务层使用,业务层只需要引入该接口的实例即可。

二、命令使用

go build -tags "tag1" 新增标签
go version -m ${buldfile} | tail -n 5 查看版本控制信息
go version -m ${buildfile} | grep tag1 查看标签信息
go mod vendor 读取mod文件,下载第三方代码到vendor文件夹中

go build -mod=vendor

go test -mod=vendor

指定使用vendor中的依赖包
go work 没有go work命令之前,本地只能存放一个版本的第三方包,如果需要使用不同的包版本,就需要在go.mod中增加replace映射,使用go work可以轻松的管理各个包之间的依赖关系,可以通过-workfile=off关闭工作区

三、常见错误

(1)拷贝错误,目的切片没有开辟空间

package main

import "fmt"

func main() {
    src := []int{0, 1, 2}
    var dst []int
    copy(dst, src)
    fmt.Println(dst)
}
// 正确代码
func main() {
    src := []int{0, 1, 2}
    dst := make([]int, len(src)) // 分配长度与 src 相同的切片
    copy(dst, src)
    fmt.Println(dst)
}

(2)在range中循环使用指针元素的影响

package main

import "fmt"

func main() {
    items := []int{10, 20, 30}
    var pointers []*int

    for _, item := range items {
        pointers = append(pointers, &item)
    }

    for _, p := range pointers {
        fmt.Println(*p)
    }
}
// 输出
30
30
30

两种解决方法,在循环内部再创建一个临时变量

func main() {
    items := []int{10, 20, 30}
    var pointers []*int

    for _, item := range items {
        t := item
        pointers = append(pointers, &t)
    }

    for _, p := range pointers {
        fmt.Println(*p)
    }
}
// 输出 10 20 30

或者直接使用真实元素的地址

func main() {
    items := []int{10, 20, 30}
    var pointers []*int

    for i := range items {
        pointers = append(pointers, &items[i])
    }

    for _, p := range pointers {
        fmt.Println(*p)
    }
}
// 输出 10 20 30

或者在Go 1.21中,使用环境变量GOEXPERIMENT=loopvar能够控制每次循环都会创建新的循环变量。

四、Dao层参考结构

type (
    Entity struct {
        ID     int64 `gorm:"column:id;type:bigint(20) unsigned;primary_key;AUTO_INCREMENT;comment:自增主键" json:"id"`
    }

    QueryCond struct {
        Entity
        QueryOptions mysql.QueryOptions
    }

    DaoObj struct {
        GormDB *gorm.DB
    }
)

为了方便接口和代码分离,可以专门定义一个接口文件,用来定义Dao层需要实现的各种方法,比如定义一个api.go文件,在该文件中定义API接口。

type OpenAPI interface {
    Modify(ctx context.Context, e *Entity) (error)
}    

定义OpenAPI之后,为了约束某个Dao层对象完全实现所定义的API 接口,可以使用类型赋值的方式进行检测。

var (
    _ OpenAPI = (*DaoObj)(nil)
)

 这段代码的含义如下:将nil值转换为*DaoObj类型,nil代表*DaoObj类型的零值,转换为*DaoObj类型之后,赋值给OpenAPI类型的_,这里使用_作为变量名,是因为这里只需要做类型检查,而不需要保存这个变量,如果*DaoObj类型没有完全实现OpenAPI里面定义的方法,这里赋值就会报错,方便进行错误提示和检查。

在service层调用dao层服务时,根据DaoObj进行相关操作,如果Dao有多个,即有多个数据表Model时,可以将多个DaoModel集中到一个Service对象中,定义新的SvrModel类型,

type (
    SvrModel struct {
        DaoModel1 *daoModel1.DaoObj1
        DaoModel2 *daoModel2.DaoObj2
        DaoModel3 *daoModel3.DaoObj3
    }
)

定义一个方法构造SvrModel指针类型的对象,对DaoModel1,DaoModel2,DaoModel3进行赋值,因此Dao层也需要方法能够构造DaoObj2,DaoObj2,DaoObj3对指针类型。

另外,在事物中,尽量不要放查询操作,这种耗时的操作应该尽量放在开启事物之前,使用协程进行并行查询。其次,Dao层之间不要互相引用,每个Dao层只完成自己对应表的操作,如果需要多个表操作,放在service层中进行。

其他常量或者结构体,可以单独定义在schema(模式、架构)中,举一个例子如下:

type ListReq struct {
    Page     uint   `form:"page,default=1" json:"page,omitempty"`
}
  • form:"page,default=1"

    • form:这部分指明这个字段在解析来自表单(Form)数据时的行为。
    • page:这告诉解析器,当从表单数据中读取时,应该将对应的表单值映射到这个 Page 字段。
    • default=1:这表示如果在表单数据中没有找到 page,则默认将 Page 字段的值设置为 1
  • json:"page,omitempty"

    • json:这部分指定了在进行 JSON 序列化或反序列化时如何处理这个字段。
  • page:这指出在 JSON 数据中对应的键是 page
  • omitempty:这是一个选项,表示如果字段 Page 的值是零值(对于 uint 类型,零值是 0),则在生成的 JSON 数据中省略这个字段。这有助于减少 JSON 数据的大小,特别是在数据传输或存储时。

五、定义API层

每个业务模块建立一个文件夹,比如业务模块A和B分别用来完成针对业务模块A和B的任务,在每个业务模块中,定义三个go文件,一个go文件用来专门定义API接口和常用的请求返回结构体,大概代码样式:

package atask
import "context"

type (
    APIer interface {
        Add(ctx context.Context, record string) error
        Delete(ctx context.Context, id int) error
        Update(ctx context.Context, id int, record string) error
        Query(ctx context.Context, id int) (queryRes, error)
    }
    
    queryRes struct {
        ID int64 `json:"id"`
        Record string `json:"msg"`
    }
)

另一个go文件中,负责提供创建对象的方法,比如提供New方法,可以返回一个结构体对象,要求这个结构体对象完成APIer的接口,一般在定义个结构体,用该结构体接收数据,将该结构体作为数据部分封装到另外一个结构体中,提供创建该结构体的方法。要让该结构体有API功能,需要将该结构体对象复制为API类型结构体。

type (
    API struct {
        APIer
    }
    
    Opts struct {
        Sk string `yaml:"sk"`
        AK string `yaml:"ak"`
    }

    objAPI struct {
        opts *Opts
    }
)

func NewOptions(ctx context.Context) (opts *Opts, error) {}
func NewobjAPI(opts *Opts) APIer {
    return &objAPI{
        opts: opts,
    }
}
        

func New(o *Opts) (APIer, error) {
    return &API{NewobjAPI(o)}, nil
}

第三个go文件则负责实现具体的API接口,

func (objapi *objAPI) Add(...)...
func (objapi *objAPI) Delete(...)...
func (objapi *objAPI) Update(...)...
func (objapi *objAPI) Query(...)...


网站公告

今日签到

点亮在社区的每一天
去签到