前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >写在学习golang一个月后

写在学习golang一个月后

作者头像
李海彬
发布于 2018-10-08 06:49:21
发布于 2018-10-08 06:49:21
1.1K00
代码可运行
举报
文章被收录于专栏:Golang语言社区Golang语言社区
运行总次数:0
代码可运行

原文作者:闫大伯

遇到的问题

连接池。由于PHP没有连接池,当高并发时就会有大量的数据库连接直接冲击到MySQL上,最终导致数据库挂掉。虽然Swoole有连接池,但是Swoole只是PHP的一个扩展,之前使用Swoole过程中就踩过很多的坑。经过我们的讨论还是觉得使用Golang更加可控一些。

框架的选择

在PHP中一直用的是Yaf,所以在Go中自然而言就选择了Gin。因为我们一直以来的原则是:尽量接近底层代码。

封装过于完善的框架不利于对整个系统的掌控及理解。我不需要你告诉我这个目录是干嘛的,这个配置怎么写,这个函数怎么用等等。

Gin是一个轻路由框架,很符合我们的需求。为了更好地开发,我们也做了几个中间件

中间件——input

每个接口都需要获取GET或POST的参数,但是gin自带的方法只能返回string,所以我们进行了简单的封装。封装过后我们就可以根据所需直接转换成想要的数据类型。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1package input
 2
 3import (
 4    "strconv"
 5)
 6
 7type I struct {
 8    body string
 9}
10
11func (input *I) get(p string) *I {
12    d, e := Context.GetQuery(p)
13    input.body = d
14    if e == false {
15        return input
16    }
17
18    return input
19}
20
21func (input *I) post(p string) *I {
22    d, e := Context.GetPostForm(p)
23    input.body = d
24    if e == false {
25        return input
26    }
27
28    return input
29}
30
31func (input *I) String() string {
32    return input.body
33}
34
35func (input *I) Atoi() int {
36    body, _ := strconv.Atoi(input.body)
37    return body
38}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1package input
 2
 3//获取GET参数
 4func Get(p string) *I {
 5    i := new(I)
 6    return i.get(p)
 7}
 8
 9//获取POST参数
10func Post(p string) *I {
11    i := new(I)
12    return i.get(p)
13}

封装之前

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1pid, _ := strconv.Atoi(c.Query("product_id"))
2alias := c.Query("product_alias")

封装之后

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1pid := input.Get("product_id").Atoi()
2alias := input.Get("product_alias").String()

中间件——logger

gin自身的logger比较简单,一般我们都需要将日志按日期分文件写到某个目录下。所以我们自己重写了一个logger,这个logger可以实现将日志按日期分文件并将错误信息发送给Sentry。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1package ginx
 2
 3import (
 4    "fmt"
 5    "io"
 6    "os"
 7    "time"
 8
 9    "github.com/gin-gonic/gin"
10    "sao.cn/configs"
11)
12
13var (
14    logPath string
15    lastDay int
16)
17
18func init() {
19    logPath = configs.Load().Get("SYS_LOG_PATH").(string)
20    _, err := os.Stat(logPath)
21    if err != nil {
22        os.Mkdir(logPath, 0755)
23    }
24}
25
26func defaultWriter() io.Writer {
27    writerCheck()
28    return gin.DefaultWriter
29}
30
31func defaultErrorWriter() io.Writer {
32    writerCheck()
33    return gin.DefaultErrorWriter
34}
35
36func writerCheck() {
37    nowDay := time.Now().Day()
38    if nowDay != lastDay {
39        var file *os.File
40        filename := time.Now().Format("2006-01-02")
41        logFile := fmt.Sprintf("%s/%s-%s.log", logPath, "gosapi", filename)
42
43        file, _ = os.Create(logFile)
44        if file != nil {
45            gin.DefaultWriter = file
46            gin.DefaultErrorWriter = file
47        }
48    }
49
50    lastDay = nowDay
51}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  1package ginx
  2
  3import (
  4    "bytes"
  5    "encoding/json"
  6    "errors"
  7    "fmt"
  8    "io"
  9    "net/url"
 10    "time"
 11
 12    "github.com/gin-gonic/gin"
 13    "gosapi/application/library/output"
 14    "sao.cn/sentry"
 15)
 16
 17func Logger() gin.HandlerFunc {
 18    return LoggerWithWriter(defaultWriter())
 19}
 20
 21func LoggerWithWriter(outWrite io.Writer) gin.HandlerFunc {
 22    return func(c *gin.Context) {
 23        NewLog(c).CaptureOutput().Write(outWrite).Report()
 24    }
 25}
 26
 27const (
 28    LEVEL_INFO  = "info"
 29    LEVEL_WARN  = "warning"
 30    LEVEL_ERROR = "error"
 31    LEVEL_FATAL = "fatal"
 32)
 33
 34type Log struct {
 35    startAt time.Time
 36    conText *gin.Context
 37    writer  responseWriter
 38    error   error
 39
 40    Level     string
 41    Time      string
 42    ClientIp  string
 43    Uri       string
 44    ParamGet  url.Values `json:"pGet"`
 45    ParamPost url.Values `json:"pPost"`
 46    RespBody  string
 47    TimeUse   string
 48}
 49
 50func NewLog(c *gin.Context) *Log {
 51    bw := responseWriter{buffer: bytes.NewBufferString(""), ResponseWriter: c.Writer}
 52    c.Writer = &bw
 53
 54    clientIP := c.ClientIP()
 55    path := c.Request.URL.Path
 56    method := c.Request.Method
 57    pGet := c.Request.URL.Query()
 58    var pPost url.Values
 59    if method == "POST" {
 60        c.Request.ParseForm()
 61        pPost = c.Request.PostForm
 62    }
 63    return &Log{startAt: time.Now(), conText: c, writer: bw, Time: time.Now().Format(time.RFC850), ClientIp: clientIP, Uri: path, ParamGet: pGet, ParamPost: pPost}
 64}
 65
 66func (l *Log) CaptureOutput() *Log {
 67    l.conText.Next()
 68    o := new(output.O)
 69    json.Unmarshal(l.writer.buffer.Bytes(), o)
 70    switch {
 71    case o.Status_code != 0 && o.Status_code < 20000:
 72        l.Level = LEVEL_ERROR
 73        break
 74    case o.Status_code > 20000:
 75        l.Level = LEVEL_WARN
 76        break
 77    default:
 78        l.Level = LEVEL_INFO
 79        break
 80    }
 81
 82    l.RespBody = l.writer.buffer.String()
 83    return l
 84}
 85
 86func (l *Log) CaptureError(err interface{}) *Log {
 87    l.Level = LEVEL_FATAL
 88    switch rVal := err.(type) {
 89    case error:
 90        l.RespBody = rVal.Error()
 91        l.error = rVal
 92        break
 93    default:
 94        l.RespBody = fmt.Sprint(rVal)
 95        l.error = errors.New(l.RespBody)
 96        break
 97    }
 98
 99    return l
100}
101
102func (l *Log) Write(outWriter io.Writer) *Log {
103    l.TimeUse = time.Now().Sub(l.startAt).String()
104    oJson, _ := json.Marshal(l)
105    fmt.Fprintln(outWriter, string(oJson))
106    return l
107}
108
109func (l *Log) Report() {
110    if l.Level == LEVEL_INFO || l.Level == LEVEL_WARN {
111        return
112    }
113
114    client := sentry.Client()
115    client.SetHttpContext(l.conText.Request)
116    client.SetExtraContext(map[string]interface{}{"timeuse": l.TimeUse})
117    switch {
118    case l.Level == LEVEL_FATAL:
119        client.CaptureError(l.Level, l.error)
120        break
121    case l.Level == LEVEL_ERROR:
122        client.CaptureMessage(l.Level, l.RespBody)
123        break
124    }
125}

由于Gin是一个轻路由框架,所以类似数据库操作和Redis操作并没有相应的包。这就需要我们自己去选择好用的包。

Package - 数据库操作

最初学习阶段使用了datbase/sql,但是这个包有个用起来很不爽的问题。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1pid := 10021
 2rows, err := db.Query("SELECT title FROM `product` WHERE id=?", pid)
 3if err != nil {
 4    log.Fatal(err)
 5}
 6defer rows.Close()
 7for rows.Next() {
 8    var title string
 9    if err := rows.Scan(&title); err != nil {
10        log.Fatal(err)
11    }
12    fmt.Printf("%s is %d\n", title, pid)
13}
14if err := rows.Err(); err != nil {
15    log.Fatal(err)
16}

上述代码,如果select的不是title,而是*,这时就需要提前把表结构中的所有字段都定义成一个变量,然后传给Scan方法。

这样,如果一张表中有十个以上字段的话,开发过程就会异常麻烦。那么我们期望的是什么呢。提前定义字段是必须的,但是正常来说应该是定义成一个结构体吧? 我们期望的是查询后可以直接将查询结果转换成结构化数据

花了点时间寻找,终于找到了这么一个包——github.com/jmoiron/sqlx。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1// You can also get a single result, a la QueryRow
 2    jason = Person{}
 3    err = db.Get(&jason, "SELECT * FROM person WHERE first_name=$1", "Jason")
 4    fmt.Printf("%#v\n", jason)
 5    // Person{FirstName:"Jason", LastName:"Moiron", Email:"jmoiron@jmoiron.net"}
 6
 7    // if you have null fields and use SELECT *, you must use sql.Null* in your struct
 8    places := []Place{}
 9    err = db.Select(&places, "SELECT * FROM place ORDER BY telcode ASC")
10    if err != nil {
11        fmt.Println(err)
12        return
13    }

sqlx其实是对database/sql的扩展,这样一来开发起来是不是就爽多了,嘎嘎~

为什么不用ORM? 还是上一节说过的,尽量不用过度封装的包。

Package - Redis操作

最初我们使用了redigo【github.com/garyburd/redigo/redis】,使用上倒是没有什么不爽的,但是在压测的时候发现一个问题,即连接池的使用。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1func factory(name string) *redis.Pool {
 2    conf := config.Get("redis." + name).(*toml.TomlTree)
 3    host := conf.Get("host").(string)
 4    port := conf.Get("port").(string)
 5    password := conf.GetDefault("passwd", "").(string)
 6    fmt.Printf("conf-redis: %s:%s - %s\r\n", host, port, password)
 7
 8    pool := &redis.Pool{
 9        IdleTimeout: idleTimeout,
10        MaxIdle:     maxIdle,
11        MaxActive:   maxActive,
12        Dial: func() (redis.Conn, error) {
13            address := fmt.Sprintf("%s:%s", host, port)
14            c, err := redis.Dial("tcp", address,
15                redis.DialPassword(password),
16            )
17            if err != nil {
18                exception.Catch(err)
19                return nil, err
20            }
21
22            return c, nil
23        },
24    }
25    return pool
26}
27
28/**
29 * 获取连接
30 */
31func getRedis(name string) redis.Conn {
32    return redisPool[name].Get()
33}
34
35/**
36 * 获取master连接
37 */
38func Master(db int) RedisClient {
39    client := RedisClient{"master", db}
40    return client
41}
42
43/**
44 * 获取slave连接
45 */
46func Slave(db int) RedisClient {
47    client := RedisClient{"slave", db}
48    return client
49}

以上是定义了一个连接池,这里就产生了一个问题,在redigo中执行redis命令时是需要自行从连接池中获取连接,而在使用后还需要自己将连接放回连接池。最初我们就是没有将连接放回去,导致压测的时候一直压不上去。

那么有没有更好的包呢,答案当然是肯定的 —— gopkg.in/redis.v5

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1func factory(name string) *redis.Client {
 2    conf := config.Get("redis." + name).(*toml.TomlTree)
 3    host := conf.Get("host").(string)
 4    port := conf.Get("port").(string)
 5    password := conf.GetDefault("passwd", "").(string)
 6    fmt.Printf("conf-redis: %s:%s - %s\r\n", host, port, password)
 7
 8    address := fmt.Sprintf("%s:%s", host, port)
 9    return redis.NewClient(&redis.Options{
10        Addr:        address,
11        Password:    password,
12        DB:          0,
13        PoolSize:    maxActive,
14    })
15}
16
17/**
18 * 获取连接
19 */
20func getRedis(name string) *redis.Client {
21    return factory(name)
22}
23
24/**
25 * 获取master连接
26 */
27func Master() *redis.Client {
28    return getRedis("master")
29}
30
31/**
32 * 获取slave连接
33 */
34func Slave() *redis.Client {
35    return getRedis("slave")
36}

可以看到,这个包就是直接返回需要的连接了。

那么我们去看一下他的源码,连接有没有放回去呢。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1func (c *baseClient) conn() (*pool.Conn, bool, error) {
 2    cn, isNew, err := c.connPool.Get()
 3    if err != nil {
 4        return nil, false, err
 5    }
 6    if !cn.Inited {
 7        if err := c.initConn(cn); err != nil {
 8            _ = c.connPool.Remove(cn, err)
 9            return nil, false, err
10        }
11    }
12    return cn, isNew, nil
13}
14
15func (c *baseClient) putConn(cn *pool.Conn, err error, allowTimeout bool) bool {
16    if internal.IsBadConn(err, allowTimeout) {
17        _ = c.connPool.Remove(cn, err)
18        return false
19    }
20
21    _ = c.connPool.Put(cn)
22    return true
23}
24
25func (c *baseClient) defaultProcess(cmd Cmder) error {
26    for i := 0; i <= c.opt.MaxRetries; i++ {
27        cn, _, err := c.conn()
28        if err != nil {
29            cmd.setErr(err)
30            return err
31        }
32
33        cn.SetWriteTimeout(c.opt.WriteTimeout)
34        if err := writeCmd(cn, cmd); err != nil {
35            c.putConn(cn, err, false)
36            cmd.setErr(err)
37            if err != nil && internal.IsRetryableError(err) {
38                continue
39            }
40            return err
41        }
42
43        cn.SetReadTimeout(c.cmdTimeout(cmd))
44        err = cmd.readReply(cn)
45        c.putConn(cn, err, false)
46        if err != nil && internal.IsRetryableError(err) {
47            continue
48        }
49
50        return err
51    }
52
53    return cmd.Err()
54}

可以看到,在这个包中的底层操作会先去connPool中Get一个连接,用完之后又执行了putConn方法将连接放回connPool。

结束语

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 1package main
 2
 3import (
 4    "github.com/gin-gonic/gin"
 5
 6    "gosapi/application/library/initd"
 7    "gosapi/application/routers"
 8)
 9
10func main() {
11    env := initd.ConfTree.Get("ENVIRONMENT").(string)
12    gin.SetMode(env)
13
14    router := gin.New()
15    routers.Register(router)
16
17    router.Run(":7321") // listen and serve on 0.0.0.0:7321
18}

3月21日开始写main,现在已经上线一个星期了,暂时还没发现什么问题。

经过压测对比,在性能上提升了大概四倍左右。原先响应时间在70毫秒左右,现在是10毫秒左右。原先的吞吐量大概在1200左右,现在是3300左右。

虽然Go很棒,但是我还是想说:PHP是最好的语言!(这句虽是原话,但小编持保留意见哈哈哈哈~)


版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-08-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Golang语言社区 微信公众号,前往查看

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
gin框架之log处理
我们自定义了log的记录方式,但是这种方式国语简单,不利于我们收集错误信息。在这里为大家推荐一个记录日志的库`github.com/sirupsen/logrus`。logrus是目前Github上star数量最多的日志库.
大话swift
2020/03/26
8.9K0
Golang学习笔记之日志log、zap
(1)Golang's log模块主要提供了3类接口。分别是 “Print 、Panic 、Fatal ”,对每一类接口其提供了3中调用方式,分别是 "Xxxx 、Xxxxln 、Xxxxf",基本和fmt中的相关函数类似。
李海彬
2018/12/29
2.3K0
Go中日志库
Gin框架的请求日志默认在控制台输出,但更多的时候,尤其上线运行时,我们希望将用户的请求日志保存到日志文件中,以便更好的分析与备份。
码客说
2024/03/29
1790
Go中日志库
Golang工程经验(上)
作为一个C/C++的开发者而言,开启Golang语言开发之路是很容易的,从语法、语义上的理解到工程开发,都能够快速熟悉起来;相比C、C++,Golang语言更简洁,更容易写出高并发的服务后台系统
李海彬
2018/10/08
2K1
Golang工程经验(上)
Go WebSocket + Redis 实现轻量级的订阅和实时消息推送
上一篇介绍了Golang中封装WebSocket功能,让WebSocket更简单好用和稳定。
杨永贞
2021/01/26
4.1K0
Go WebSocket + Redis 实现轻量级的订阅和实时消息推送
Go Web开发框架基本组成
本文目的实现在Go语言开发,搭建基本的框架信息和较优秀的组件使用介绍给大家。从而可以快速上手Go语言开发工作。
刘銮奕
2020/12/30
1.2K0
Go实战项目-Beego的Session、日志文件的使用和redis的选择使用
go标准库里面没有实现这功能,只能靠自己实现了,哦,不,是第三方库。好在beego就自带session功能,这个之前就说过了。我们只是简单使用下,高并发场景估计还得自己来实现,单纯的靠这个框架,够呛。来看下怎么使用: 1、在调用之前就需要开启 beego.BConfig.WebConfig.Session.SessionOn = true //开始session beego目前支持四种session的存储引擎 memory、file、Redis 和 MySQL 默认就是memory ,但是,你重启之后就失效了,这除了写demo可以用之外,就算是保活的进程也是很肉痛,基于之前PHP框架保存文件的处理方式,我这边也是存放文件中。 2、设置存储引擎 beego.BConfig.WebConfig.Session.SessionProvider = “file” //指定文件存储方式 3、设置存储路径 beego.BConfig.WebConfig.Session.SessionProviderConfig = “./.tmp” //指定文件存储路径地址,也可以不指定,有默认的地址。 建议,存储的文件夹名称加上“.”,这样方便git提交的时候直接过滤,但是一般情况下,没事不要去下载,或者放在项目以外的其他路径也是可以的。这样就是永久保存了,重启依然有效。
用户6680840
2022/06/08
1.9K0
使用 AI 辅助开发一个开源 IP 信息查询工具:一
在写代码时,总是会遇到一些有趣的机缘巧合。前几天,我在翻看自己之前的开源项目时,又看到了 DDNS 相关的讨论。虽然在 2021 年我写过两篇相对详细的教程:《使用 Nginx 提供 DDNS 服务(前篇)[1]》和《使用 Nginx 提供 DDNS 服务(中篇)[2]》,但总觉得还可以做得更好。
soulteary
2024/12/23
1680
使用 AI 辅助开发一个开源 IP 信息查询工具:一
Golang高性能日志库zap + lumberjack 日志切割组件详解
Zap提供了两种类型的日志记录器 — Sugared Logger 和 Logger
传说之下的花儿
2023/04/16
5.4K0
Golang高性能日志库zap + lumberjack 日志切割组件详解
[系列] - go-gin-api 路由中间件 - 日志记录(三)
上篇文章分享了,规划项目目录和参数验证,其中参数验证使用的是 validator.v8 版本,现已更新到 validator.v9 版本,最新代码查看 github 即可。
新亮
2019/09/03
2.9K0
[系列] - go-gin-api 路由中间件 - 日志记录(三)
使用 AI 辅助开发一个开源 IP 信息查询工具:一
在写代码时,总是会遇到一些有趣的机缘巧合。前几天,我在翻看自己之前的开源项目时,又看到了 DDNS 相关的讨论。虽然在 2021 年我写过两篇相对详细的教程:《使用 Nginx 提供 DDNS 服务(前篇)》和《使用 Nginx 提供 DDNS 服务(中篇)》,但总觉得还可以做得更好。
soulteary
2024/12/22
1180
使用 AI 辅助开发一个开源 IP 信息查询工具:一
Gin-Web-Framework官方指南中文(下篇)
ShouldBind,ShouldBindJSON,ShouldBindXML,ShouldBindQuery,ShouldBindYAML
小诚信驿站
2019/10/31
2.4K0
Gin-Web-Framework官方指南中文(下篇)
golang源码分析:go-mysql(1)
https://github.com/go-mysql-org/go-mysql是一个实现了mysql协议和binlog协议的工具库,可以用来实现主从复制(Replication),增量同步(Incremental dumping),客户端(Client),虚拟服务端(Fake server),高可用(Failover),mysql的驱动(database/sql like driver),前面介绍的golang源码分析:mysql同步工具gravity(2)就是基于这个库实现的。下面我们开始体验下如何使用这个库。
golangLeetcode
2023/09/06
7420
golang源码分析:go-mysql(1)
Zap日志库并集成Gin
在许多Go语言项目中,我们需要一个好的日志记录器能够提供下面这些功能: 1 . 能够将事件记录到文件中,而不是应用程序控制台; 2 . 日志切割-能够根据文件大小、时间或间隔等来切割日志文件; 3 . 支持不同的日志级别。例如INFO,DEBUG,ERROR等; 4 . 能够打印基本信息,如调用文件/函数名和行号,日志时间等;
iginkgo18
2021/04/23
3.6K0
redis客户端对比redigo go-redis
应用程序调用Get方法从池中获取连接,并使用连接的Close方法将连接的资源返回到池。
golangLeetcode
2022/08/02
1.6K0
redis客户端对比redigo go-redis
golang 源码分析:go-redis
go-redis 的代码实现是一个明显的命令模式,命令模式的优点有将调用操作的对象与知道如何实现该操作的对象解耦,增加新的命令不需要修改现有的类。go-redis 支撑单节点,哨兵模式和集群模式,每一种模式对应的命令接口其实几乎一样的,go-redis就是通过命令模式将命令和每一种client的实现解耦的。
golangLeetcode
2022/08/03
6650
Go语言WEB框架之Gin
文档:https://gin-gonic.com/zh-cn/docs/quickstart/
码客说
2022/10/05
1.3K0
Gin框架 - 日志记录
查了很多资料,Go 的日志记录用的最多的还是 github.com/sirupsen/logrus。
新亮
2019/07/22
3.5K0
2.Go语言编程学习课后实践
示例1.利用reflect反射实现一个ini配置文件的读取 (ini 文件解析器)。
全栈工程师修炼指南
2022/09/29
7550
2.Go语言编程学习课后实践
一行报错,让我探究起了go-redis连接池
关于连接池,想必大家耳熟能详。从其定义上来说,连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用。简单点来说,就是当我们的程序在运行时,将数据库的连接进行实例化,每个连接当成对象存储在内存中,并且用一个数量大小的池子将其管理起来,当后续需要与数据库进行网络通信的时候再从池子中取出已有且正常的连接对象进行复用即可。因此,其所带来的好处显而易见,比如:1.减少连接的创建时间;2.提高资源的复用性减少资源浪费;3.精简编程模式简化开发模型等 ..... 在刚入职从事后端开发的时候,就听前辈们说过我们的项目使用了数据库的连接池模型,而当时也一直没有深入的去理解和研究连接池底层的原理以及实现,而就在上周,突然发现服务器的日志上,多了一条redis连接池的报错日志,其内容如下图所示:
_春华秋实
2024/07/10
4260
相关推荐
gin框架之log处理
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档