前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从别人的代码中学习golang系列--01

从别人的代码中学习golang系列--01

作者头像
coders
发布2020-07-03 14:39:20
7550
发布2020-07-03 14:39:20
举报
文章被收录于专栏:coder修行路coder修行路

自己最近在思考一个问题,如何让自己的代码质量逐渐提高,于是想到整理这个系列,通过阅读别人的代码,从别人的代码中学习,来逐渐提高自己的代码质量。本篇是这个系列的第一篇,我也不知道自己会写多少篇,但是希望自己能坚持下去。

第一个自己学习的源码是:https://github.com/LyricTian/gin-admin

自己整理的代码地址:https://github.com/peanut-pg/gin_admin 这篇文章整理的时候只是为了跑起来整体的代码,对作者的代码进行精简。

这篇博客主要是阅读gin-admin的第一篇,整理了从代码项目目录到日志库使用中学习到的内容:

  1. 项目目录规范
  2. 配置文件的加载
  3. github.com/sirupsen/logrus 日志库在项目的使用
  4. 项目的优雅退出
  5. Golang的选项模式

项目目录规范

作者的项目目录还是非常规范的,应该也是按照https://github.com/golang-standards/project-layout 规范写的,这个规范虽然不是官方强制规范的,但是确实很多开源项目都在采用的,所以我们在生产中正式的项目都应该尽可能遵循这个目录规范标准进行代码的编写。关于这个目录的规范使用,自己会在后续实际使用中逐渐完善。

/cmd

main函数文件(比如 /cmd/myapp.go)目录,这个目录下面,每个文件在编译之后都会生成一个可执行的文件。

不要把很多的代码放到这个目录下面,这里面的代码尽可能简单。

/internal

应用程序的封装的代码。我们的应用程序代码应该放在 /internal/app 目录中。而这些应用程序共享的代码可以放在 /internal/pkg目录中

/pkg

一些通用的可以被其他项目所使用的代码,放到这个目录下面。

/vendor

应用程序的依赖项,go mod vendor 命令可以创建vendor目录。

Service Application Directories

/api

协议文件,Swagger/thrift/protobuf

Web Application Directories

/web

web服务所需要的静态文件

Common Application Directories

/configs

配置文件目录

/init

系统的初始化

/scripts

用于执行各种构建,安装,分析等操作的脚本。

/build

打包和持续集成

/deployments

部署相关的配置文件和模板

/test

其他测试目录,功能测试,性能测试等

Other Directories

/docs

设计和用户文档

/tools

常用的工具和脚本,可以引用 /internal 或者 /pkg 里面的库

/examples

应用程序或者公共库使用的一些例子

/assets

其他一些依赖的静态资源

配置文件的加载

作者的gin-admin 项目中配置文件加载库使用的是:github.com/koding/multiconfig

在这之前,听到使用最多的就是大名鼎鼎的viper ,但是相对于viper相对来说就比较轻便了,并且功能还是非常强大的。感兴趣的可以看看这篇文章:https://sfxpt.wordpress.com/2015/06/19/beyond-toml-the-gos-de-facto-config-file/

栗子

程序目录结构为:

代码语言:javascript
复制
configLoad
├── configLoad
├── config.toml
└── main.go

通过一个简单的例子来看看multiconfig的使用

代码语言:javascript
复制
package main

import (
	"fmt"

	"github.com/koding/multiconfig"
)

type (
	// Server holds supported types by the multiconfig package
	Server struct {
		Name     string
		Port     int `default:"6060"`
		Enabled  bool
		Users    []string
		Postgres Postgres
	}

	// Postgres is here for embedded struct feature
	Postgres struct {
		Enabled           bool
		Port              int
		Hosts             []string
		DBName            string
		AvailabilityRatio float64
	}
)

func main() {
	m := multiconfig.NewWithPath("config.toml") // supports TOML and JSON

	// Get an empty struct for your configuration
	serverConf := new(Server)

	// Populated the serverConf struct
	m.MustLoad(serverConf) // Check for error

	fmt.Println("After Loading: ")
	fmt.Printf("%+v\n", serverConf)

	if serverConf.Enabled {
		fmt.Println("Enabled field is set to true")
	} else {
		fmt.Println("Enabled field is set to false")
	}
}

配置文件config.toml内容为:

代码语言:javascript
复制
Name              = "koding"
Enabled           = false
Port              = 6066
Users             = ["ankara", "istanbul"]

[Postgres]
Enabled           = true
Port              = 5432
Hosts             = ["192.168.2.1", "192.168.2.2", "192.168.2.3"]
AvailabilityRatio = 8.23

编译之后执行,效果如下:

代码语言:javascript
复制
➜  configLoad ./configLoad 
After Loading: 
&{Name:koding Port:6066 Enabled:false Users:[ankara istanbul] Postgres:{Enabled:true Port:5432 Hosts:[192.168.2.1 192.168.2.2 192.168.2.3] DBName: AvailabilityRatio:8.23}}
Enabled field is set to false
➜  configLoad ./configLoad -h
Usage of ./configLoad:
  -enabled
        Change value of Enabled. (default false)
  -name
        Change value of Name. (default koding)
  -port
        Change value of Port. (default 6066)
  -postgres-availabilityratio
        Change value of Postgres-AvailabilityRatio. (default 8.23)
  -postgres-dbname
        Change value of Postgres-DBName.
  -postgres-enabled
        Change value of Postgres-Enabled. (default true)
  -postgres-hosts
        Change value of Postgres-Hosts. (default [192.168.2.1 192.168.2.2 192.168.2.3])
  -postgres-port
        Change value of Postgres-Port. (default 5432)
  -users
        Change value of Users. (default [ankara istanbul])

Generated environment variables:
   SERVER_ENABLED
   SERVER_NAME
   SERVER_PORT
   SERVER_POSTGRES_AVAILABILITYRATIO
   SERVER_POSTGRES_DBNAME
   SERVER_POSTGRES_ENABLED
   SERVER_POSTGRES_HOSTS
   SERVER_POSTGRES_PORT
   SERVER_USERS

flag: help requested
➜  configLoad ./configLoad -name=test
After Loading: 
&{Name:test Port:6066 Enabled:false Users:[ankara istanbul] Postgres:{Enabled:true Port:5432 Hosts:[192.168.2.1 192.168.2.2 192.168.2.3] DBName: AvailabilityRatio:8.23}}
Enabled field is set to false
➜  configLoad export SERVER_NAME="test_env"
➜  configLoad ./configLoad                 
After Loading: 
&{Name:test_env Port:6066 Enabled:false Users:[ankara istanbul] Postgres:{Enabled:true Port:5432 Hosts:[192.168.2.1 192.168.2.2 192.168.2.3] DBName: AvailabilityRatio:8.23}}
Enabled field is set to false

从上面的使用中,你能能够看到,虽然multiconfig 非常轻量,但是功能还是非常强大的,可以读配置文件,还可以通过环境变量,以及我们常用的命令行模式。

日志库在项目的使用

这个可能对很多初学者来说都是非常有用的,因为一个项目中,我们基础的就是要记录日志,golang有很多强大的日志库,如:作者的gin-admin 项目使用的github.com/sirupsen/logrus; 还有就是uber开源的github.com/uber-go/zap等等

这里主要学习一下作者是如何在项目中使用logrus,这篇文章对作者使用的进行了精简。当然只是去掉了关于gorm,以及mongo的hook的部分,如果你的项目中没有使用这些,其实也先不用关注这两个hook部分的代码,不影响使用,后续的系列文章也会对hook部分进行整理。

作者封装的logger库是在pkg/loggger目录中,我精简之后如下:

代码语言:javascript
复制
package logger

import (
	"context"
	"fmt"
	"io"
	"os"
	"time"

	"github.com/sirupsen/logrus"
)

// 定义键名
const (
	TraceIDKey      = "trace_id"
	UserIDKey       = "user_id"
	SpanTitleKey    = "span_title"
	SpanFunctionKey = "span_function"
	VersionKey      = "version"
	StackKey        = "stack"
)

// TraceIDFunc 定义获取跟踪ID的函数
type TraceIDFunc func() string

var (
	version     string
	traceIDFunc TraceIDFunc
	pid         = os.Getpid()
)

func init() {
	traceIDFunc = func() string {
		return fmt.Sprintf("trace-id-%d-%s",
			os.Getpid(),
			time.Now().Format("2006.01.02.15.04.05.999999"))
	}
}

// Logger 定义日志别名
type Logger = logrus.Logger

// Hook 定义日志钩子别名
type Hook = logrus.Hook

// StandardLogger 获取标准日志
func StandardLogger() *Logger {
	return logrus.StandardLogger()
}

// SetLevel设定日志级别
func SetLevel(level int) {
	logrus.SetLevel(logrus.Level(level))
}

// SetFormatter 设定日志输出格式
func SetFormatter(format string) {
	switch format {
	case "json":
		logrus.SetFormatter(new(logrus.JSONFormatter))
	default:
		logrus.SetFormatter(new(logrus.TextFormatter))
	}
}

// SetOutput 设定日志输出
func SetOutput(out io.Writer) {
	logrus.SetOutput(out)
}

// SetVersion 设定版本
func SetVersion(v string) {
	version = v
}

// SetTraceIDFunc 设定追踪ID的处理函数
func SetTraceIDFunc(fn TraceIDFunc) {
	traceIDFunc = fn
}

// AddHook 增加日志钩子
func AddHook(hook Hook) {
	logrus.AddHook(hook)
}

type (
	traceIDKey struct{}
	userIDKey  struct{}
)

// NewTraceIDContext 创建跟踪ID上下文
func NewTraceIDContext(ctx context.Context, traceID string) context.Context {
	return context.WithValue(ctx, traceIDKey{}, traceID)
}

// FromTraceIDContext 从上下文中获取跟踪ID
func FromTraceIDContext(ctx context.Context) string {
	v := ctx.Value(traceIDKey{})
	if v != nil {
		if s, ok := v.(string); ok {
			return s
		}
	}
	return traceIDFunc()
}

// NewUserIDContext 创建用户ID上下文
func NewUserIDContext(ctx context.Context, userID string) context.Context {
	return context.WithValue(ctx, userIDKey{}, userID)
}

// FromUserIDContext 从上下文中获取用户ID
func FromUserIDContext(ctx context.Context) string {
	v := ctx.Value(userIDKey{})
	if v != nil {
		if s, ok := v.(string); ok {
			return s
		}
	}
	return ""
}

type spanOptions struct {
	Title    string
	FuncName string
}

// SpanOption 定义跟踪单元的数据项
type SpanOption func(*spanOptions)

// SetSpanTitle 设置跟踪单元的标题
func SetSpanTitle(title string) SpanOption {
	return func(o *spanOptions) {
		o.Title = title
	}
}

// SetSpanFuncName 设置跟踪单元的函数名
func SetSpanFuncName(funcName string) SpanOption {
	return func(o *spanOptions) {
		o.FuncName = funcName
	}
}

// StartSpan 开始一个追踪单元
func StartSpan(ctx context.Context, opts ...SpanOption) *Entry {
	if ctx == nil {
		ctx = context.Background()
	}
	var o spanOptions
	for _, opt := range opts {
		opt(&o)
	}
	fields := map[string]interface{}{
		VersionKey: version,
	}
	if v := FromTraceIDContext(ctx); v != "" {
		fields[TraceIDKey] = v
	}
	if v := FromUserIDContext(ctx); v != "" {
		fields[UserIDKey] = v
	}
	if v := o.Title; v != "" {
		fields[SpanTitleKey] = v
	}
	if v := o.FuncName; v != "" {
		fields[SpanFunctionKey] = v
	}

	return newEntry(logrus.WithFields(fields))

}

// Debugf 写入调试日志
func Debugf(ctx context.Context, format string, args ...interface{}) {
	StartSpan(ctx).Debugf(format, args...)
}

// Infof 写入消息日志
func Infof(ctx context.Context, format string, args ...interface{}) {
	StartSpan(ctx).Infof(format, args...)
}

// Printf 写入消息日志
func Printf(ctx context.Context, format string, args ...interface{}) {
	StartSpan(ctx).Printf(format, args...)
}

// Warnf 写入警告日志
func Warnf(ctx context.Context, format string, args ...interface{}) {
	StartSpan(ctx).Warnf(format, args...)
}

// Errorf 写入错误日志
func Errorf(ctx context.Context, format string, args ...interface{}) {
	StartSpan(ctx).Errorf(format, args...)
}

// Fatalf 写入重大错误日志
func Fatalf(ctx context.Context, format string, args ...interface{}) {
	StartSpan(ctx).Fatalf(format, args...)
}

// ErrorStack 输出错误栈
func ErrorStack(ctx context.Context, err error) {
	StartSpan(ctx).WithField(StackKey, fmt.Sprintf("%+v", err)).Errorf(err.Error())
}

// Entry 定义统一的日志写入方式
type Entry struct {
	entry *logrus.Entry
}

func newEntry(entry *logrus.Entry) *Entry {
	return &Entry{entry: entry}
}

func (e *Entry) checkAndDelete(fields map[string]interface{}, keys ...string) *Entry {
	for _, key := range keys {
		_, ok := fields[key]
		if ok {
			delete(fields, key)
		}
	}
	return newEntry(e.entry.WithFields(fields))
}

// WithFields 结构化字段写入
func (e *Entry) WithFields(fields map[string]interface{}) *Entry {
	e.checkAndDelete(fields,
		TraceIDKey,
		SpanTitleKey,
		SpanFunctionKey,
		VersionKey)
	return newEntry(e.entry.WithFields(fields))
}

// WithField 结构化字段写入
func (e *Entry) WithField(key string, value interface{}) *Entry {
	return e.WithFields(map[string]interface{}{key: value})
}

// Fatalf 重大错误日志
func (e *Entry) Fatalf(format string, args ...interface{}) {
	e.entry.Fatalf(format, args...)
}

// Errorf 错误日志
func (e *Entry) Errorf(format string, args ...interface{}) {
	e.entry.Errorf(format, args...)
}

// Warnf 警告日志
func (e *Entry) Warnf(format string, args ...interface{}) {
	e.entry.Warnf(format, args...)
}

// Infof 消息日志
func (e *Entry) Infof(format string, args ...interface{}) {
	e.entry.Infof(format, args...)
}

// Printf 消息日志
func (e *Entry) Printf(format string, args ...interface{}) {
	e.entry.Printf(format, args...)
}

// Debugf 写入调试日志
func (e *Entry) Debugf(format string, args ...interface{}) {
	e.entry.Debugf(format, args...)
}

通过Logrus & Context 实现了统一的 TraceID/UserID 等关键字段的输出,从这里也可以看出来,作者这样封装非常也符合符合pkg目录的要求,我们可以很容易的在internal/app 目录中使用封装好的logger。接着就看一下如何使用,作者在internal/app 目录下通过logger.go 中的InitLogger进行日志的初始化,设置了日志的级别,日志的格式,以及日志输出文件。这样我们在internal/app的其他包文件中只需要导入pkg下的logger即可以进行日志的记录。

项目的优雅退出

在这里不得不提的就是一个基础知识Linux信号signal,推荐看看https://blog.csdn.net/lixiaogang_theanswer/article/details/80301624

linux系统中signum.h中有对所有信号的宏定义,这里注意一下,我使用的是manjaro linux,我的这个文件路径是/usr/include/bits/signum.h 不同的linux系统可能略有差别,可以通过find / -name signum.h 查找确定

代码语言:javascript
复制
#define SIGSTKFLT       16      /* Stack fault (obsolete).  */
#define SIGPWR          30      /* Power failure imminent.  */

#undef  SIGBUS
#define SIGBUS           7
#undef  SIGUSR1
#define SIGUSR1         10
#undef  SIGUSR2
#define SIGUSR2         12
#undef  SIGCHLD
#define SIGCHLD         17
#undef  SIGCONT
#define SIGCONT         18
#undef  SIGSTOP
#define SIGSTOP         19
#undef  SIGTSTP
#define SIGTSTP         20
#undef  SIGURG
#define SIGURG          23
#undef  SIGPOLL
#define SIGPOLL         29
#undef  SIGSYS
#define SIGSYS          31

在linux终端可以使用kill -l 查看所有的信号

代码语言:javascript
复制
➜  ~ kill -l
HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS
➜  ~ 

信号说明:

信号

起源

默认行为

含义

SIGHUP

POSIX

Term

控制终端挂起

SIGINT

ANSI

Term

键盘输入以终端进程(ctrl + C)

SIGQUIT

POSIX

Core

键盘输入使进程退出(Ctrl + \)

SIGILL

ANSI

Core

非法指令

SIGTRAP

POSIX

Core

断点陷阱,用于调试

SIGABRT

ANSI

Core

进程调用abort函数时生成该信号

SIGIOT

4.2BSD

Core

和SIGABRT相同

SIGBUS

4.2BSD

Core

总线错误,错误内存访问

SIGFPE

ANSI

Core

浮点异常

SIGKILL

POSIX

Term

终止一个进程。该信号不可被捕获或被忽略

SIGUSR1

POSIX

Term

用户自定义信号之一

SIGSEGV

ANSI

Core

非法内存段使用

SIGUSR2

POSIX

Term

用户自定义信号二

SIGPIPE

POSIX

Term

往读端关闭的管道或socket链接中写数据

SIGALRM

POSIX

Term

由alarm或settimer设置的实时闹钟超时引起

SIGTERM

ANSI

Term

终止进程。kill命令默认发生的信号就是SIGTERM

SIGSTKFLT

Linux

Term

早期的Linux使用该信号来报告数学协处理器栈错误

SIGCLD

System V

Ign

和SIGCHLD相同

SIGCHLD

POSIX

Ign

子进程状态发生变化(退出或暂停)

SIGCONT

POSIX

Cont

启动被暂停的进程(Ctrl+Q)。如果目标进程未处于暂停状态,则信号被忽略

SIGSTOP

POSIX

Stop

暂停进程(Ctrl+S)。该信号不可被捕捉或被忽略

SIGTSTP

POSIX

Stop

挂起进程(Ctrl+Z)

SIGTTIN

POSIX

Stop

后台进程试图从终端读取输入

SIGTTOU

POSIX

Stop

后台进程试图往终端输出内容

SIGURG

4.3 BSD

Ign

socket连接上接收到紧急数据

SIGXCPU

4.2 BSD

Core

进程的CPU使用时间超过其软限制

SIGXFSZ

4.2 BSD

Core

文件尺寸超过其软限制

SIGVTALRM

4.2 BSD

Termhttps://github.com/LyricTian/gin-admin

与SIGALRM类似,不过它只统计本进程用户空间代码的运行时间

SIGPROF

4.2 BSD

Term

与SIGALRM 类似,它同时统计用户代码和内核的运行时间

SIGWINCH

4.3 BSD

Ign

终端窗口大小发生变化

SIGPOLL

System V

Term

与SIGIO类似

SIGIO

4.2 BSD

Term

IO就绪,比如socket上发生可读、可写事件。因为TCP服务器可触发SIGIO的条件很多,故而SIGIO无法在TCP服务器中用。SIGIO信号可用在UDP服务器中,但也很少见

SIGPWR

System V

Thttps://github.com/LyricTian/gin-adminerm

对于UPS的系统,当电池电量过低时,SIGPWR信号被触发

SIGSYS

POSIX

Core

非法系统调用

SIGUNUSED

Core

保留,通常和SIGSYS效果相同

作者中代码是这样写的:

代码语言:javascript
复制
func Run(ctx context.Context, opts ...Option) error {
	var state int32 = 1
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	cleanFunc, err := Init(ctx, opts...)
	if err != nil {
		return err
	}

EXIT:
	for {
		sig := <-sc
		logger.Printf(ctx, "接收到信号[%s]", sig.String())
		switch sig {
		case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
			atomic.CompareAndSwapInt32(&state, 1, 0)
			break EXIT
		case syscall.SIGHUP:
		default:
			break EXIT
		}
	}
    // 进行一些清理操作
	cleanFunc()
	logger.Printf(ctx, "服务退出")
	time.Sleep(time.Second)
	os.Exit(int(atomic.LoadInt32(&state)))
	return nil
}

这里监听了syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT,当收到这些信号的,则会进行系统的退出,同时在程序退出之前进行一些清理操作。

这里我们有一个知识点需要回顾一下:golang中的break label 和 goto label

  • break label,break的跳转标签(label)必须放在循环语句for前面,并且在break label跳出循环不再执行for循环里的代码。break标签只能用于for循环
  • goto label的label(标签)既可以定义在for循环前面,也可以定义在for循环后面,当跳转到标签地方时,继续执行标签下面的代码。

Golang的选项模式

其实在很多的开源项目中都可以看到golang 选项模式的使用,推荐看看https://www.sohamkamani.com/golang/options-pattern/

假如我们现在想要建造一个房子,我们思考,建造房子需要木材,水泥......,假如我们现在就想到这两个,我们用代码实现:

代码语言:javascript
复制
type House struct {
	wood   string // 木材
	cement string // 水泥
}

// 造一个房子
func NewHouse(wood, cement shttps://github.com/LyricTian/gin-admintring) *House {
	house := &House{
		wood:   wood,
		cement: cement,
	}
	return house
}

上面这种方式,应该是我们经常可以看到的实现方式,但是这样实现有一个很不好的地方,就是我们突然发现我们建造房子海需要钢筋,这个时候我们就必须更改NewHouse 函数的参数,并且NewHouse的参数还有顺序依赖。

对于扩展性来说,上面的这种实现放那格式其实不是非常好,而golang的选项模式很好的解决了这个问题。

栗子

代码语言:javascript
复制
type HouseOption func(*House)

type House struct {
	Wood   string // 木材
	Cement string // 水泥
}

func WithWood(wood string) HouseOption {
	return func(house *House) {
		house.Wood = wood
	}
}

func WithCement(Cement string) HouseOption {
	return func(house *House) {
		house.Cement = Cement
	}
}

// 造一个新房子
func NewHouse(opts ...HouseOption) *House {
	h := &House{}
	for _, opt := range opts {
		opt(h)
	}
	return h
}

这样当我们这个时候发现,我建造房子还需要石头,只需要在House结构体中增加对应的字段,同时增加一个

WithStone 函数即可,同时我们我们调用NewHouse的地方也不会有顺序依赖,只需要增加一个参数即可,更改之后的代码如下:

代码语言:javascript
复制
type HouseOption func(*House)

type House struct {
	Wood   string // 木材
	Cement string // 水泥
	Stone  string // 石头
}

func WithWood(wood string) HouseOption {
	return func(house *House) {
		house.Wood = wood
	}
}

func WithCement(Cement string) HouseOption {
	return func(house *House) {
		house.Cement = Cement
	}
}

func WithStone(stone string) HouseOption {
	return func(house *House) {
		house.Stone = stone
	}
}

// 造一个新房子
func NewHouse(opts ...HouseOption) *House {
	h := &House{}
	for _, opt := range opts {
		opt(h)
	}
	return h
}

func main() {
	house := NewHouse(
		WithCement("上好的水泥"),
		WithWood("上好的木材"),
		WithStone("上好的石头"),
	)
	fmt.Println(house)https://github.com/LyricTian/gin-admin
}

选项模式小结

  • 生产中正式的并且较大的项目使用选项模式可以方便后续的扩展
  • 增加了代码量,但让参数的传递更新清晰明了
  • 在参数确实比较复杂的场景推荐使用选项模式

总结

从https://github.com/LyricTian/gin-admin 作者的这个项目中我们首先从大的方面学习了:

  1. golang 项目的目录规范
  2. github.com/koding/multiconfig 配置文件库
  3. logrus 日志库的使用
  4. 项目的优雅退出实现,信号相关知识
  5. golang的选项模式
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-07-02 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 项目目录规范
    • /cmd
      • /internal
        • /pkg
          • /vendor
            • /api
              • /web
                • /configs
                  • /init
                    • /scripts
                      • /build
                        • /deployments
                          • /test
                            • /docs
                              • /tools
                                • /examples
                                  • /assets
                                  • 配置文件的加载
                                    • 栗子
                                    • 日志库在项目的使用
                                    • 项目的优雅退出
                                    • Golang的选项模式
                                      • 栗子
                                        • 选项模式小结
                                        • 总结
                                        相关产品与服务
                                        持续集成
                                        CODING 持续集成(CODING Continuous Integration,CODING-CI)全面兼容 Jenkins 的持续集成服务,支持 Java、Python、NodeJS 等所有主流语言,并且支持 Docker 镜像的构建。图形化编排,高配集群多 Job 并行构建全面提速您的构建任务。支持主流的 Git 代码仓库,包括 CODING 代码托管、GitHub、GitLab 等。
                                        领券
                                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档