2019-09-16 公司项目golang开发指南

steven20182016 · · 1174 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一、Mac OS X Go开发环境搭建

1.安装 go

https://golang.google.cn/dl/下载对应的go安装包,然后安装,如果是macOS x需要 10.10 or later版本

2.环境配置

环境变量的配置有系统级别的和用户级别的,/etc/下的profile为系统的环境变量设置,对所有用户起作用,~/.bash_profile为当前用户的环境变量设置,只对当前用户起作用,一般我们只需要配置~/.bash_profile即可。

(1)工作空间的配置

在写go代码之前我们需要创建工作空间来存放go代码,并把工作空间的目录保存到环境变量,一般我们在$HOME目录下面创建一个go文件夹,那么工作空间的目录就是$HOME/go,如果工作空间创建在别的地方就需要设置GOPATH和GOBIN环境变量。

(2)环境变量的配置

$GOROOT 表示 Go 在你的电脑上的安装位置

$GOBIN 表示编译器和链接器的安装位置,是运行go install产生二进制文件的目录

$GOPATH 项目的工作空间的路径

$GO111MODULE :使用go mod可以方便对第三方包进行管理

根据约定,GOPATH($HOME/go)下需要创建3个目录:

bin 存储编译后的可执行文件

pkg 存放编译后生成的包文件

src 存放项目的源码

终端下面执行sudo vim ~/.bash_profile编辑~/.bash_profile ,然后 添加环境变量

export GOROOT=/usr/local/go

export GOPATH=XXX/XXX/go

export GOBIN=/XXX/XXX/go/bin

export GO111MODULE=on

为了不重启电脑刷新profile文件需在终端下面执行source ~/.bash_profile,然后终端下面执行go version查看安装版本,出现go version goX.X.X darwin/amd64,表明安装成功

可以在终端执行go env查看go 设置的环境变量

3.主要go 命令详解

go run: 一次性运行go 源码程序,把以go结尾的文件编译连接形成执行文件

go build: 编译go 源码

go install: go源码编译并打包到 $GOPATH/bin 目录下, 执行go install后如果直接执行二进制文件提示zsh: command not found:XXX,那么在~/.bash_profile添加环境变量:export PATH=$HOME/go/bin:$PATH,然后执行source ~/.bash_profile更新

上面配置完其实就可以进行go编程了,但是我们项目的开发还需要继续下面的流程。

4.go的删除

删除 /usr/local目录下的 go

删除 PATH 环境变量

在/etc/profile 或者 $HOME/.bahs_profile中删除关于go环境变量的设置

如果是通过 mac os x 的安装包安装的,那么应该删除 /etc/paths.d/go 文件

5、goland IDE安装,必须安装2018.2及以上版本支持go mod

goland IDE下使用go modules

在goland下,是推荐使用goland配置vgo来快速使用go modules的。而vgo是基于Go Modules规范的第三方包管理工具,同官方的go mod命令工具类似。对于通过goland IDE创建的工程,一定要开启go modules功能,如下图:

6、依赖包的安装go get使用配置

go 1.11 开始加入的 go module (vgo),我们可以借助go get命令来拉取或者更新代码包及依赖包,但是由于国内防火墙的原因,很多代码及依赖包不能通过go get获取,因为我们需要做一些配置来解决。go get 命令可以借助代码管理工具通过远程拉取或更新代码包及其依赖包,并自动完成编译和安装,这个命令在内部实际上分成了两步操作:第一步是下载源码包,第二步是执行 go install。为了 go get 命令能正常工作,你必须确保安装了合适的源码管理工具,并同时把这些命令加入你的 PATH 中,再使用go get获取远程包之前,请确保 GOPATH 已经设置。Go 1.8 版本之后,GOPATH 默认在用户目录的 go 文件夹.

(1)go get通过 git 下载或更新源代码

我们需要配置一下 git (当然,github 或私有仓库需要配置 ssh key,这里不详细介绍)

git config --global url."git@github.com:".insteadOf "https://github.com/"

git config --global url."git@git.querycap.com:".insteadOf "https://git.querycap.com/"

(2)go get通过golang.org获取代码,在这一阶段,会从 https://go.googlesource.com获取代码,我们需要通过github上面的镜像获取,配置git如下:

git config --global url."git@github.com:golang/".insteadOf "https://go.googlesource.com/"

配置完成可以通过git config -l查看

url.git@github.com:golang/.insteadof=https://go.googlesource.com/

url.git@git.querycap.com:.insteadof=https://git.querycap.com/

url.git@github.com:.insteadof=https://github.com/

7、依赖包管理工具go mod的使用

(1). go mod使用配置

从go 1.11开始,go支持新的依赖包管理工具go mod,由于一些第三方包国内不能下载,所以需要设置GOPROXY(默认国内不能访问的https://proxy.golang.org)和GONOSUMDB,在终端执行vim ~/.bash_profile,新加环境变量GOPROXY和GONOSUMDB

export GOPROXY=https://goproxy.cn,direct  //通过七牛云代理来下载第三方库包

export GONOSUMDB=git.querycap.com/*     //不需要哈希检查git.querycap.com/下的库包

如果GOSUMDB不为空,最好把它设置为空

(2). go mod原理

在项目的根目录下面执行go mod init,创建一个go.mod文件,当执行go文件的时候,go mod 会自动查找依赖自动下载

go mod是Go项目的依赖描述文件该文件主要用来描述两个事情:

《1》当前项目名(module)是什么。每个项目都应该设置一个名称,当前项目中的包(package)可以使用该名称进行相互调用,比如我的项目目录是在$HOME/go/src/git.querycap.com/practice/srv-demo-yzhl,那么module就是git.querycap.com/practice/srv-demo-yzhl,项目中的包引用的时候就可以import "git.querycap.com/practice/srv-demo-yzhl/XXXX"

《2》当前项目依赖的第三方包名称。项目运行时会自动分析项目中的代码依赖,生成go.sum依赖分析结果,随后go编译器会去下载这些第三方包,然后再编译运行。

go.sum依赖分析文件,记录每个依赖库的版本和哈希值

一般情况下,go.sum应当被添加到版本管理中随着go.mod文件一起提交。

   (3). go mod常用命令:

   go mod init moduleName // 初始化modules

    go mod download: //下载依赖的module到本地cache

    go mod edit //编辑go.mod文件,选项有-json、-require和-exclude,可以使用帮助go help mod edit

    go mod graph // 以文本模式打印模块需求图

    go mod tidy //检查,删除错误或者不使用的modules,以及添加缺失的模块

    go mod verify // 验证依赖是否正确

    go mod why //解释为什么需要依赖

(4). go mod 升级依赖包

 go get -u 将会升级到最新的次要版本或者修订版本 (x.y.z, z 是修订版本号 y 是次要版本号) 

 go get -u=patch 将会升级到最新的修订版本 

 go get package@version 将会升级到指定的版本

二、公司的项目目录及代码目录结构

在设置的GOPATCH里面创建src目录,然后在里面创建git.querycap.com目录,再创建组目录(比如我demo项目的组是practice),再创建项目目录(比如我的demo项目是srv-demo-yzhl)。后面我们通过项目实践来一步一步介绍代码的目录结构

三、安装常用工具包tools

安装tools(必须在go mod之后),使用go get -u git.querycap.com/tools/cmd/tools获取安装(如果获取不成功就到https://git.querycap.com/tools/cmd目录把tools pull下来放到工作空间的src/git.querycap.com/tools里面), 执行make命令安装tools,安装完成之后在终端执行tools命令看看是否成功

四、项目实践

下面我们通过一步一步构建工程来说明项目代码的目录结构及公司go项目中数据库、网络库、缓存及中间件auth的使用

1.最小项目代码结构:

在项目目录里面(比如我的demo项目的srv-messageDemo-yzhl里面)执行go mod init和建立main.go文件,添加main函数,然后在mian.go目录下面在终端执行go run main.go,最小的工程已经生成,生成的代码结构如图:

(1)config下面的default.yml配置文件保存了项目用到的配置,比如数据库的host,密码,用户;redis的host及密码,为了使用本地的数据库,需要建立local.yml(local.yml不上传到git代码仓库里面,只供本地调试使用),依次类推需要建立各个环境的配置文件,比如stage.yml(测试环境),demo.yml(演示环境),master.yml(线上环境),当前我的项目default.yml配置如下:

GOENV: DEV

(2)helmx.default.yml:配置项目基本信息,这些信息将在部署时作为环境变量配置,我的项目最终helmx.default.yml配置如下:

service: {}

(3)go.sum依赖分析文件,记录每个依赖库的版本和哈希值

git.querycap.com/tools/servicex v1.3.2  h1:lctzJQV4vg8rF7vgcC4xh4E5t9D0HYo7ZVIOzb50tkw=

git.querycap.com/tools/servicex v1.3.2/go.mod   h1:fW/KvNvHPrC6GUyNXlU1GQAu06LrdlcS92hNvCSyj7c=

github.com/BurntSushi/toml v0.3.1/go.mod  h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=

github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod   h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=

2.实现查看openapi.json文件内容的api

我们先来看通过postman查看openapi.json内容的请求如下:

要实现上述功能我们需要用到第三方的Courier微服务的库,Courier最小的单位是Operator,任何的struct都是一个Operator,Operator有一个Output(ctx context.Context) (interface{}, error)方法,struct结构定义Operator的输入参数,Output(ctx context.Context) (interface{}, error)根据输入参数返回成功或者失败的结果,Router是Operator的载体,每个Router至少包含一个Operator。下面是路由的例子说明:

先定义三个路由RouterRoot,RouterA,RouterB

var RouterRoot = courier.NewRouter(&OperatorRoot{})

var RouterA = courier.NewRouter(&OperatorA{})

var RouterB = courier.NewRouter(&OperatorB{})

在初始方法里面注册路由,把路由连接起来

func init() {

    RouterRoot.Register(RouterA)

    RouterRoot.Register(RouterB)

    RouterA.Register(courier.NewRouter(&OperatorA1{}, &OperatorA2{}))

    RouterA.Register(courier.NewRouter(&OperatorA3{}, &OperatorA4{}))

    RouterB.Register(courier.NewRouter(&OperatorB1{}, &OperatorB2{}))

}

最终得到的路由如下:

    OperatorRoot -> OperatorA -> OperatorA1 -> OperatorA2

     OperatorRoot -> OperatorA -> OperatorA3 -> OperatorA4

    OperatorRoot -> OperatorB -> OperatorB1 -> OperatorB2

那下面我们需要在工程里面先创建routers文件夹,所有后续操作的路由都在此文件夹里面创建,然后再建立路由root.go文件,内容如下:

由于我们实现的是RESTful API,所以我们操作使用的是Courier里面RESTful API的承载者http transport 里面的操作httptransport.Group("/")和httptransport.Group("/demo-yzhl")

routers完成后我们再创建global文件夹,里面创建config.go文件,在里面定义项目全局使用的变量及项目需要使用的service,目前我们只需定义Server全局变量,内容如下:

然后我们在main函数里面执行courier.Run(routers.RootRouter,global.Server),内容如下:

最后在项目根目录下面执行命令tools openapi,项目工程显示新生成的openapi.json文件,注意openapi.json不需要传到git仓库

在项目目录里面在终端执行go run main.go,输出如下信息:

然后在浏览器或者postman里面输入localhost/demo-yzhl/既可以看到上面openapi.json内容,终止程序运行同时执行ctrl+c

3.实现包含登录、注册、查询及用户校验的API

(1)由于项目中需要用到一些枚举常量,先建立constants文件夹来存放项目中使用的枚举常量,然后再建errors文件夹存放状态错误码的枚举常量,状态码为9位,组成如下:

status code serivce code auto-increased id

500               001                    001

再建立status_error.go文件,内容如下:

package errors

import "net/http"

//go:generate tools gen error StatusError

type StatusError int

func (StatusError) ServiceCode() int  {

return 999 *1e3

}

//400 错误的请求,服务器不理解请求的语法

const (

StatusBadRequestError StatusError =http.StatusBadRequest*1e6 + iota + 1

)

//401 未授权,请求需要身份验证

const (

StatusUnauthorized StatusError =http.StatusUnauthorized*1e6 + iota + 1

)

//403 禁止,服务器拒绝请求

const (

ForbiddenError StatusError =http.StatusForbidden*1e6 +iota +1

)

//404 未找到

const (

NotFoundError StatusError =http.StatusNotFound*1e6 +iota +1

  // @errTalk 用户不存在

  UserNotFound

  // @errTalk account id 不存在

)

//409 冲突

const (

ConflictError StatusError =http.StatusConflict*1e6  + iota  + 1

  // @errTalk 用户冲突

  UserConflictError

)

// 500 服务器内部错误

const (

InternalServerError StatusError =http.StatusInternalServerError*1e6  + iota + 1

)

进入errors目录在终端执行tools gen error StatusError或者点击//go:generate tools gen error StatusError这行代码左边的绿色箭头选择第三项产生可以使用的StatusError代码文件

(2)由于用户需要有唯一标志user id,所以使用client的服务来产生user id

建立clients文件夹并创建gen.go文件,gen.go文件内容为:

//go:generate tools gen client id --spec-url http://srv-id.base.d.rktl.work/id

生成代码的方式如上面生成StatusError相同,生成完后代码目录结构如下:

引入了client服务之后,我们需要使用client服务产生user id,下面我们建立modules文件夹来存放项目中的各功能模块。功能模块的go文件文件命名为功能模块名字_mgr.go,例如产生user id的go文件名为id_mgr.go,内容如下:

package id

import (

"context"

"git.querycap.com/practice/srv-demo-yzhl/clients/client_id"

"git.querycap.com/practice/srv-demo-yzhl/constants/errors"

"git.querycap.com/tools/datatypes"

"github.com/go-courier/metax"

)

//定义生成user id的结构体IDMgr

type IDMgr struct {

c client_id.ClientID

  metax.Ctx

}

//根据clentid服务创建一个IDMgr对象

func NewIDMgr(c client_id.ClientID) *IDMgr {

return &IDMgr{

c:c,

}

}

//根据上一个操作的上下文来创建一个IDMgr对象

func (idmgr *IDMgr) WithContent(ctx context.Context)  *IDMgr  {

return &IDMgr{

c:idmgr.c.WithContext(ctx),

Ctx:idmgr.Ctx.WithContext(ctx),

}

}

//生成user id

func (idmgr *IDMgr) GenerateID()  (datatypes.UUID, error)  {

resp,  _, err :=idmgr.c.GenerateID()

if err !=nil {

return 0,errors.InternalServerError

  }

return datatypes.UUID(resp.ID),nil

}

然后在global文件夹下面的config.go里面创建一个全局变量IDMgr在项目中使用,代码结构及config.go代码如下:

上面完成之后我们还需要在config.go里面初始化client服务器的配置及在local.yml里面配置client服务器的host才能使用client服务,config.go的初始化配置如下:

初始化方法init里面第一步设置服务器的名字,建议和项目名字一致,第二步设置我们需要的config,当前我们用到了Log,Server,Client配置,后面用到的redis,数据库的配置都需要在Config结构图里面添加,第三步对使用到的配置进行可用性检查。在项目目录下面执行go run main.go,发现default多了一些配置:

SRV_DEMO_YZHL__ClientID_Host:""

SRV_DEMO_YZHL__ClientID_Protocol:""

SRV_DEMO_YZHL__Log_Level: DEBUG

我们在config文件夹下面建立local.yml(程序运行的时候首先从local.yml里面读取配置,local.yml只供本地使用,不上传到git仓库),然后把上面内容复制到local.yml,并设置SRV_DEMO_YZHL__ClientID_Host,local.yml代码如下:

GOENV: DEV

SRV_DEMO_YZHL__ClientID_Host: srv-id.base.d.rktl.work

SRV_DEMO_YZHL__ClientID_Protocol:""

SRV_DEMO_YZHL__Log_Level: DEBUG

(3)我们来实现用户的注册、登录、校验、查找等功能

《1》首先我们在constans文件夹里面创建types文件夹存放项目用到的其它常量,然后新建user_state.go文件里面定义用户状态枚举常量,并生成项目使用的用户状态文件user_state__generated.go,生成方法如erros里面方法一样,代码如下:

注意:user_state.go里面的枚举常量第一个UNKNOWN前面是一个下划线,后面ENABLED和DISANLED前面都是两个,如果后面下划线是一个的话,//后面的信息(启用用户、禁用用户)就不能返回

《2》数据库的使用:用户信息的保存需要用到数据库,项目当中使用的数据库是Postgres,在使用数据库之前先在本地电脑上面安装三个软件:Postgres是数据库软件,Postman是调试api的软件,Navicat Premium是对数据库里面的表进行管理的软件

首先在项目里面新建database文件夹,然后新建databse的db.go及用户表结构的user.go,代码如下:

db.go

user.go

解释user.go:

// @def unique_index I_user_id UserID

表示为UserID建立唯一索引,索引名字为I_user_id

//go:generate tools gen model2 User -t t_yzhl_user --database DBDemo

User:产生用户表使用的struct

model2 :generate interfaces of db mode

t_yzhl_user:数据库paractice_demo_message中用户表的名字

DBDemo:paractice_demo_message

最终产生的go文件名字为user__generated.go,产生方式同上面讲的errors里面产生错误码的方式一样,user__generated.go里面包括了对用户表的操作,供我们在项目中调用。

为了使用数据库,我们需要在global文件夹里面的config.go添加数据库Postgres的配置,添加后的代码如下:

执行go run main.go, default.yml会出现Postgres的本地配置,

SRV_DEMO_YZHL__Postgres_Host: 127.0.0.1

SRV_DEMO_YZHL__Postgres_Password:""

SRV_DEMO_YZHL__Postgres_SlaveHost:""

SRV_DEMO_YZHL__Postgres_User:""

然后复制到local.yml里面

打开上面安装的Postgress和Navicat Premium,在Postgress新建一个server:

在Navicat Premium里面连接刚才建立的pracice_demo_message的server,然后在main初始化方法里面添加要执行的命令,其中migrage就是生成数据库及表的命令。

添加完后执行go run main.go migrage生成名为pracice_demo_message的db及t_yzhl_user的表,在Navicat Premium里面显示如下:

《3》在module文件夹下面新建user文件夹,里面存放user的功能模块

用户的注册功能:

先在user文件夹里面新建user_param.go,里面定义CreateUserBody结构体包含注册需要的用户名和密码

type CreateUserBody struct {

//昵称 大于等于三个字符,在json结构里面的key为nickname

  Nickname string `json:"nickname" validate:"@char[3,]"`

  //密码 大于等于6个字符,在json结构里面的key为password

  Password string `json:"password" validate:"@char[6,]"`

}

然后在user文件夹里面新建user_mgr.go,定义三个生成UserMgr的方法:

在global里面的config.go定义UserMgr全局变量供项目使用

var (

IDMgr =id.NewIDMgr(ClientID)

UserMgr =user.NewUserMgr(IDMgr,Postgres)

)

由于有注册,登录,查询等功能,我们再新建一个user_mgr_user.go文件,里面定义注册、登录、查询等用户的方法,先定义注册用户的方法CreateUser,代码如下:

《4》实现注册的RESTful API:

先在routes文件夹下面新建user文件夹,然后新建root.go及create_user.go

root.go里面定义UserRouter变量:var UserRouter =courier.NewRouter(httptransport.Group("/users"))

在routers下面的root.go的init方法里面添加路由:

RootRouter.Register(user.UserRouter)

对于RESTful API,需要http method and pattern path去分发路由

create_user.go代码如下:

在项目目录下面执行go run main.go,运行成功后:

打开Postman,新建一个post方法,body里面的nickname和password以json结构传输,URI为localhost/v0/users,点击Send,收到下面的response,到目前为止注册用户的RESTFul API就完成了。

刷新Navicat Premium,用户表显示注册的nickname1用户:

《5》实现登录的RESTFul API

步骤和上面注册的API相同,先在user_mgr_usr.go添加根据昵称和密码登录的方法FetchUserWithNicknameAndPwd:

为了便于阅读和维护,一个API一个文件,所以在routers的user文件夹里面新建login_user.go,代码如下:

其中LoginUserRouter路由在user文件夹里面的root.go定义

package user

import (

"github.com/go-courier/courier"

"github.com/go-courier/httptransport"

)

func init()  {

UserRouter.Register(LoginUserRouter)

}

var UserRouter =courier.NewRouter(httptransport.Group("/users"))

var LoginUserRouter =courier.NewRouter(httptransport.Group("/login"))

登录成功之后生成token我们实用到了第三方库jwt,jwt的使用大家可以自行百度,所以同生成user id功能类似,我们需要在module文件夹下面新建token文件夹,里面新建token_mgr.go来生成token,代码如下:

然后在global的config.go里面定义TokenMgr全局变量供项目使用:

var (

IDMgr =id.NewIDMgr(ClientID)

UserMgr =user.NewUserMgr(IDMgr,Postgres)

TokenMgr =token.NewTokenMgr(JwtSecretKey)

)

在项目目录下面执行go run main.go命令,终端显示信息如下:

根据终端的输出信息显示注册和登录都是post方法,数据都是放body里面,如果上面不重新定义LoginUserRouter,注册和登录的URI都是localhost/v0/users,这是不允许的,所以我们重新定义了LoginUserRouter路由

在Postman里面测试登录的API:

《6》实现根据用户的userID查找用户的API:

查找用户的时候需要对header里面的token进行验证,所以需要用到中间件authorization。在routers下面新建middleware文件夹,里面新建auth_user.go,代码如下:

package middleware

import (

"context"

"git.querycap.com/practice/srv-demo-yzhl/constants/errors"

"git.querycap.com/practice/srv-demo-yzhl/databases"

"git.querycap.com/practice/srv-demo-yzhl/global"

"git.querycap.com/tools/authorization"

"github.com/dgrijalva/jwt-go"

"reflect"

"strconv"

)

type ContextAccountAuth struct {

//Bearer access_token

  Authorization string `name:"Authorization,omitempty" in:"header"`

}

var contextAccountAuthKey =reflect.TypeOf(ContextAccountAuth{}).String()

func (req ContextAccountAuth)ContextKey()string  {

return  contextAccountAuthKey

}

func GetUserFromContext(c context.Context) *databases.User  {

return c.Value(contextAccountAuthKey).(*databases.User)

}

func (req ContextAccountAuth)Output(ctx context.Context) (interface{},error)  {

var userID databases.UUID

  accountStr :=authorization.ParseAuthorization(req.Authorization).Get("Bearer")

if accountStr !="" {

tokenMgr :=global.TokenMgr.WithContent(ctx)

claims,err :=tokenMgr.ParseToken(accountStr)

if err !=nil {

return nil,err

      }

tempUserID,_ :=strconv.ParseUint(claims.(jwt.MapClaims)["userID"].(string),10,64)

userID =databases.UUID(tempUserID)

if userID ==0 {

return nil,errors.AccountIDNotFoundError

      }

user,err :=global.UserMgr.WithContext(ctx).FetchUserByUserID(userID)

if err !=nil {

return nil,err

      }

return user,nil

  }

return nil,errors.TokenInValidError

}

其中tokenMgr.ParseToken,在token_mgr.go,代码如下:

FetchUserByUserID在user_mgr_user.go里面:

中间件的功能完成之后,下面的根据用户的userID查找用户的实现方式和注册、登录用户流程一样:

上面已经在user_mgr_user.go里面实现了根据用户的userID查找用户的功能FetchUserByUserID,下面我们在routers下面的user里面新建实现根据userID查找用户的api的文件get_user.go

其中AuthUserRouter在user下面的root.go定义,截止目前root.go代码如下:

终端执行go run main.go:

在Postman测试根据用户的id查找用户:

到此为止,我们实现的用户注册、登录、查找功能就实现完毕,当中用到了数据库、网络库、中间件authorization使用

五、CI初始化

在项目目录下面执行:go get -u git.querycap.com/infra/hx,然后执行

hx init,项目目录下面多了helms.project.yml

创建.gitlab-ci.yml为持续集成使用

六、创建makefile编译使用:touch makefile

七、创建.gitignore文件:touch .gitignore, 在里面添加需要忽略的文件

openapi.json,config/local.yml


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:steven20182016

查看原文:2019-09-16 公司项目golang开发指南

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1174 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传