golang编程规范2

3.1.10 接口名
【规则3.12】接口名必须为大小写混排,支持包外引用则首字母大写,仅包内使用则首字母小写。不能有下划线,整体必须为名词。
【建议3.13】最好以“er”结尾,除非有更合适的单词。
例如:
1. type Reader interface {...}


3.1.11 方法接收者名
【规则3.13】方法接收名必须为大小写混排,首字母小写。方法接收者命名要能够体现接收者对象。
【建议3.14】接收者名通常1个或者2个字母就够,最长不能超过4个字母。


例如:
1. func (c *Controller) Run(stopCh <-chan struct{})
参考:https://github.com/golang/go/wiki/CodeReviewComments#receiver-names


【建议3.15】接收者名不要使用me,this 或者 self 这种泛指的名字。
【建议3.16】定义方法时,如果方法内不会直接引用接收者,则省略掉接收者名。
举例:
1. func (T) sayHi() {
2.     // do things without T
3. }
4.
5. func (*T) sayHello() {
6.     // do things without *T
7. }




3.1.12 返回值
【规则3.14】返回值如果是命名的,则必须大小写混排,首字母小写。
【建议3.17】函数的返回值应避免使用命名的参数。
举例:
1. func (n *Node) Bad() (node *Node, err error)
2. func (n *Node) Good() (*Node, error)
因为如果使用命名变量很容易导致临时变量覆盖而引起隐藏的bug。


例外情况:多个返回值类型相同的情况下,使用命名返回值来区分不同的返回参数。
说明:命名返回值使代码更清晰,同时更加容易读懂。
举例:
1. func getName()(firstName, lastName, nickName string){
2.     firstName = "May"
3.     lastName = "Chen"
4.     nickName = "Babe"
5.     return
6. }


参考:https://github.com/golang/go/wiki/CodeReviewComments#named-result-parameters
https://golang.org/doc/effective_go.html#named-results


【规则3.15】函数返回值个数不要超过3个。
【建议3.18】如果函数的返回值超过3个,建议将其中关系密切的返回值参数封装成一个结构体。


3.1.13 魔鬼数字
【规则3.16】代码中禁止使用魔鬼数字。
说明:直接使用数字,造成代码难以理解,也难以维护。应采用有意义的静态变量或枚举来代替。


例外情况:有些特殊情况下,如循环或比较时采用数字0,-1,1,这些情况可采用数字。


3.2 代码格式化要求
go默认已经有了gofmt工具,如果使用sublime、LiteIDE等goIDE工具,可以在IDE中自动格式化代码。除此之外,还有一些规范是需要开发者自行遵守的。


【规则3.17】运算符前后、逗号后面、if后面等需有单空格隔开。
1) if err != nil {…}
2) c := a + b
3) return {}, err
例外情况:
go fmt认为应该删除空格的场景。例如,在传参时,字符串拼接的”+”号。


【规则3.18】相对独立的程序块之间、变量说明之后必须加空行,而逻辑紧密相关的代码则放在一起。


不好的例子:
1. func formatResponseBody(res *http.Response, httpreq *httplib.BeegoHttpRequest, pretty bool) string {
2.     body, err := httpreq.Bytes()
3.     if err != nil {
4.         log.Fatalln("can't get the url", err)
5.     }
6.     match, err := regexp.MatchString(contentJsonRegex, res.Header.Get("Content-Type"))
7.     if err != nil {
8.         log.Fatalln("failed to compile regex", err)
9.     }
10.     if pretty && match {
11.         var output bytes.Buffer
12.         err := json.Indent(&output, body, "", "  ")
13.         if err != nil {
14.             log.Fatal("Response Json Indent: ", err)
15.         }
16.         return output.String()
17.     }
18.     return string(body)
19. }


应该改为:
1. func formatResponseBody(res *http.Response, httpreq *httplib.BeegoHttpRequest, pretty bool) string {
2.     body, err := httpreq.Bytes()
3.     if err != nil {
4.         log.Fatalln("can't get the url", err)
5.     }
6.
7.     match, err := regexp.MatchString(contentJsonRegex, res.Header.Get("Content-Type"))
8.     if err != nil {
9.         log.Fatalln("failed to compile regex", err)

10.     }

11.
12.     if pretty && match {
13.         var output bytes.Buffer
14.         err := json.Indent(&output, body, "", "  ")
15.         if err != nil {
16.             log.Fatal("Response Json Indent: ", err)
17.         }
18.
19.         return output.String()
20.     }
21.
22.     return string(body)
23. }
提示:当你需要为接下来的代码增加注释的时候,说明该考虑加一行空行了。


【规则3.19】尽早return:一旦有错误发生,马上返回。
举例:不要使用
1. if err != nil {
2.     // error handling
3. } else {
4.     // normal code
5. }


而推荐使用:
1. if err != nil {
2.      // error handling
3.      return // or continue, etc.
4. }
5.
6. // normal code
这样可以减少嵌套深度,代码更加美观。


【规则3.20】单行语句不能过长,如不能拆分需要分行写。一行最多120个字符。
换行时有如下建议:
换行时要增加一级缩进,使代码可读性更好;
低优先级操作符处划分新行;换行时操作符应保留在行尾; 
换行时建议一个完整的语句放在一行,不要根据字符数断行


示例:
1. if ((tempFlag == TestFlag) &&
2. (((counterVar - constTestBegin) % constTestModules) >= constTestThreshold)) { 
3.     // process code 
4. }


【建议3.19】单个文件长度不超过500行。
对开源引入代码可以降低约束,新增代码必须遵循。


【建议3.20】单个函数长度不超过50行。
函数两个要求:单一职责、要短小


【规则3.21】单个函数圈复杂度最好不要超过10,禁止超过15。
说明:圈复杂度越高,代码越复杂,就越难以测试和维护,同时也说明函数职责不单一。


【建议3.21】函数中缩进嵌套必须小于等于3层。
举例,禁止出现以下这种锯齿形的函数:
1. func testUpdateOpts PushUpdateOptions) (err error) {
2.     isNewRef := opts.OldCommitID == git.EMPTY_SHA
3.     isDelRef := opts.NewCommitID == git.EMPTY_SHA
4.     if isNewRef && isDelRef {
5.         if isDelRef {
6.             repo, err := GetRepositoryByName(owner.ID, opts.RepoName)
7.             if err != nil {
8.                 if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) {
9.                     if err := CommitRepoAction(CommitRepoActionOptions{
10.                         PusherName:  opts.PusherName,
11.                         RepoOwnerID: owner.ID,
12.                         RepoName:    repo.Name,
13.                         RefFullName: opts.RefFullName,
14.                         OldCommitID: opts.OldCommitID,
15.                         NewCommitID: opts.NewCommitID,
16.                         Commits:     &PushCommits{},
17.                     }); err != nil {
18.                         return fmt.Errorf("CommitRepoAction (tag): %v", err)
19.                     }
20.                     return nil
21.                 }
22.             }
23.             else {
24.                 owner, err := GetUserByName(opts.RepoUserName)
25.                 if err != nil {
26.                     return fmt.Errorf("GetUserByName: %v", err)
27.                 }
28.
29.                 return nil
30.             }
31.         }
32.     }
33.
34.     // other code
35. }
提示:如果发现锯齿状函数,应通过尽早通过return等方法重构。


【原则3.2】保持函数内部实现的组织粒度是相近的。
举例,不应该出现如下函数:
1. func main() {
2.     initLog()
3.
4.     //这一段代码的组织粒度,明显与其他的不均衡
5.     orm.DefaultTimeLoc = time.UTC

6.     sqlDriver := beego.AppConfig.String("sqldriver")

7.     dataSource := beego.AppConfig.String("datasource")
8.     modelregister.InitDataBase(sqlDriver, dataSource)
9.
10.     Run()
11. }


应该改为:
1. func main() {
2.     initLog()
3.
4.     initORM()  //修改后,函数的组织粒度保持一致 
5.
6.     Run()
7. }


【建议3.22】禁止出现2处及以上的重复代码。
如果出现,必须抽取为独立小函数。不要担心性能问题,编译器会帮你搞定大部分的内联优化。同时认真阅读第四章节的“代码质量保证优先原则”


【建议3.23】if条件判断, 同时使用超过3个表达式以上的时候, 使用switch替代。
例如:
1. if a == 0 || a == 1 || a == 2 || a == 3 {
2.     // ...
3. }


建议改写为:
1. switch a {
2. case 0, 1, 2, 3:
3.     // ....
4. }


【建议3.24】定义bool变量时,要避免判断时出现双重否定,应使用肯定形式的表达式。
举例:
1. if !notFailed && !isReported { // 晦涩,不容易理解
2.     notifyUser()
3. } else {
4.     process()
5. }


应改为:
1. if isSuccess || isReported {
2.     process()
3. } else {
4.     notifyUser()
5. }


【建议3.25】for循环初始值从0开始,判断条件使用<无等号的方式。
举例:
1. for i := 1; i <= 10; i++ {
2.     doSomeThing()
3. }


应改为:
1. for i := 0; i < 10; i++ {
2.     doSomeThing()
3. }
这样子可以迅速准确得出循环次数。


3.3 注释
Go提供了C风格的块注释/* */和C++风格的行注释//。通常为行注释;块注释大多数作为程序包的注释,但也可以用于一个表达式中,或者用来注释掉一大片代码。


godoc用来处理Go源文件,抽取有关程序包内容的文档。在顶层声明之前出现,并且中间没有换行的注释,会随着声明一起被抽取,作为该项的解释性文本。这些注释的本质和风格决定了godoc所产生文档的质量。


Go代码的注释可以被godocs工具转化为文档发布。所以准确的代码注释除了能够帮助阅读代码还有助于代码手册的生成。


Godoc工具说明可参考如下链接:
https://godoc.org/golang.org/x/tools/cmd/godoc


3.3.1 通用注释要求
【原则3.3】编写代码首先考虑如何代码自我解释,然后才是添加注释进行补充说明
说明:优秀的代码不写注释也可轻易读懂,注释无法把糟糕的代码变好,需要很多注释来解释的代码往往存在坏味道,需要重构。


示例:注释不能消除代码的坏味道:
1. // 判断m是否为素数
2. // 返回值:: 1是素数,0不是素数
3. func p(m int) int {
4.     var i, k int
5.
6.     k = sqrt(m)
7.     for i = 2; i <= k; i++ {
8.         if m%i == 0 {
9.             break // 发现整除,表示m不为素数,结束遍历
10.         }
11.     }
12.
13.     // 遍历中没有发现整除的情况,返回
14.     if i > k {
15.         return 1
16.     }
17.
18.     // 遍历中没有发现整除的情况,返回
19.     return 0
20. }


重构代码后,不需要注释:
1. // IsPrimeNumber return true if num is prime
2. func IsPrimeNumber(num int) bool {
3.     var i int
4.     sqrtOfNum := sqrt(num)
5.

6.     for i = 2; i <= sqrtOfNum; i++ {

7.         if num%i == 0 {
8.             return false
9.         }
10.     }
11.
12.     return true
13. }


【原则3.4】注释的内容要清楚、明了,含义准确,防止注释二义性。
说明:有歧义的注释反而会导致维护者更难看懂代码,正如带两块表反而不知道准确时间。
示例:注释与代码相矛盾,注释内容也不清楚,前后矛盾。
1. // 上报网管时要求故障ID与恢复ID相一致
2. // 因此在此由告警级别获知是不是恢复ID 
3. // 若是恢复ID则设置为ClearId,否则设置为AlarmId 
4. if ClearAlarmLevel != rcData.level {
5.     SetAlarmID(rcData.AlarmId);
6. } else {
7.     SetAlarmID(rcData.ClearId);
8. }


正确做法:修改注释描述如下:
1. // 网管达成协议:上报故障ID与恢复ID由告警级别确定,若是清除级别,ID设置为ClearId,否则设为AlarmId
2.
3. ...


【原则3.5】在代码的功能、意图层次上进行注释,即注释用于解释代码难以直接表达的意图,而不是重复描述代码。


说明:注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。
对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。
注释不是为了名词解释(what),而是说明用途(why)。


示例:如下注释纯属多余。
1. i++ // increment i
2. if receiveFlag { // if receiveFlag is TRUE
3.
4. ...


如下这种无价值的注释不应出现(空洞的笑话,无关紧要的注释)。
1. // 时间有限,现在是:04,根本来不及想为什么,也没人能帮我说清楚
2. ...


而如下的注释则给出了有用的信息:
1. // 由于xx编号网上问题,在xx情况下,芯片可能存在写错误,此芯片进行写操作后,必须进行回读校验,如果回读不正确,需要再重复写-回读操作,最多重复三次,这样可以解决绝大多数网上应用时的写错误问题
2. time := 0
3.
4. for (readReg(someAddr) != value) && (time < 3) {
5.    writeReg(someAddr, value)
6.    time++
7. }


对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释,出彩的或复杂的代码块前要加注释,如:
1. // Divide result by two, taking into account that x contains the carry from the add.
2. for i := 0; i < len(result); i++ {
3.     x = (x << 8) + result[i]
4.     result[i] = x >> 1
5.     x &= 1
6. }


【规则3.22】所有导出对象都需要注释说明其用途;非导出对象根据情况进行注释。必须时,应该说明值的取值范围,及默认值。
【规则3.23】注释的单行长度不能超过 80 个字符。
【规则3.24】注释需要紧贴对应的包声明和函数之前,不能有空行、
【规则3.25】非跨度很长的注释,尽量使用 // 方式。
1. /*
2. *    1. 确保 template 存在
3. */


改成:
1. // 1. 确保 template 存在


【规则3.26】避免多余的空格,两句注释之间保持一个空格。
示例:
1. // 采用这样的方式
2. // Sentence one. Sentence two.
3.
4. // 而不是如下的方式
5. // Sentence one.  Sentence two.
保持和Go的风格一样,参考https://golang.org/cl/20022


【原则3.6】注释第一条语句应该为一条概括语句,并且使用被声明的名字作为开头。
例如:
1. // Compile parses a regular expression and returns, if successful, a Regexp
2. // object that can be used to match against text.
3. func Compile(str string) (regexp *Regexp, err error) {


 【建议3.26】//与注释的文档之间空一格。
示例:
1. // 采用如下方式
2. // This is a comment
3. // for humans.
4.
5. //而不要采用如下方式:
6. //This is a comment
7. //for humans.


对于Go保留的语法,就不需要空一格
1. //go:generate go run gen.go


详细的语法可以参考:https://golang.org/cmd/compile/#hdr-Compiler_Directives.


3.3.2 包注释要求

【规则3.27】每个程序包都应该有一个包注释,一个位于package子句之前的块注释。


对于有多个文件的程序包,包注释只需要出现在一个文件中,任何一个文件都可以。包注释应该用来介绍该程序包,并且提供与整个程序包相关的信息。它将会首先出现在godoc页面上,并会建立后续的详细文档。
1. /*
2. Package regexp implements a simple library for regular expressions.
3.
4. The syntax of the regular expressions accepted is:
5.
6.     regexp:
7.         concatenation { '|' concatenation }
8.     concatenation:
9.         { closure }
10.     closure:
11.         term [ '*' | '+' | '?' ]
12.     term:
13.         '^'
14.         '$'
15.         '.'
16.         character
17.         '[' [ '^' ] character-ranges ']'
18.         '(' regexp ')'
19. */
20. package regexp


如果程序包很简单,则包注释可以非常简短。
1. // Package path implements utility routines for
2. // manipulating slash-separated filename paths.


【规则3.28】不要依靠用空格进行对齐。
注释不需要额外的格式,例如星号横幅。生成的输出甚至可能会不按照固定宽度的字体进行展现,所以不要依靠用空格进行对齐—godoc,就像gofmt,会处理这些事情。注释是不作解析的普通文本,所以HTML和其它注解,例如_this_,将会逐字的被复制。对于缩进的文本,godoc确实会进行调整,来按照固定宽度的字体进行显示,这适合于程序片段。fmt package的包注释使用了这种方式来获得良好的效果。


根据上下文,godoc甚至可能不会重新格式化注释,所以要确保它们看起来非常直接:使用正确的拼写,标点,以及语句结构,将较长的行进行折叠,等等。


3.3.3 结构、接口及其他类型注释要求
【建议3.27】类型定义一般都以单数信息描述。
示例:
1. // Request represents a request to run a command.
2. type Request struct { ...


如果为接口,则一般以以下形式描述。
示例:
1. // FileInfo is the interface that describes a file and is returned by Stat and Lstat.
2. type FileInfo interface { ...


3.3.4 函数与方法注释要求
【建议3.28】函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、设计约束等
说明:重要的、复杂的函数,提供外部使用的接口函数应编写详细的注释。


【建议3.29】如果函数或者方法为判断类型(返回值主要为bool类型),则以 <name> returns true if 开头。
如下例所示:
1. // HasPrefix returns true if name has any string in given slice as // prefix.
2. func HasPrefix(name string, prefixes []string) bool { ...




3.3.5 变量和常量的注释要求
Go的声明语法允许对声明进行组合。单个的文档注释可以用来介绍一组相关的常量或者变量。由于展现的是整个声明,这样的注释通常非常简单的。
1. // Error codes returned by failures to parse an expression.
2. var (
3.     ErrInternal      = errors.New("regexp: internal error")
4.     ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
5.     ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
6.     ...
7. )


一般建议采用这样的方式:
1. var (
2.     // BConfig is the default config for Application
3.     BConfig *Config
4.
5.     // AppConfig is the instance of Config, store the config information from file
6.     AppConfig *beegoAppConfig
7.
8.     // AppPath is the absolute path to the app
9.     AppPath string
10.
11.     // GlobalSessions is the instance for the session manager
12.     GlobalSessions *session.Manager
13. )


3.3.6 其他注释要求
 当某个部分等待完成时,可用 TODO: 开头的注释来提醒维护人员。
 当某个部分存在已知问题进行需要修复或改进时,可用 FIXME: 开头的注释来提醒维护人员。
 当需要特别说明某个问题时,可用 NOTE: 开头的注释:
 针对代码中出现的bug,可以采用BUG(who):注释,这些注释将被识别为已知的bug,并包含在文档的BUGS区。而其中的who应该是那些可以提供关于这个BUG更多信息的用户名。


比如,下面就是一个bytes包中已知的问题:
1. // BUG(r): The rule Title uses for word boundaries does not handle Unicode punctuation properly.


3.4 错误
【规则3.29】导出的错误变量的命名,以Err开始,如ErrSomething,无需导出的错误变量命名,以Error作为后缀,如specificError


举例:
1. // 包级别的导出error.
2. var ErrSomething = errors.New("something went wrong")
3.
4. func main() {
5.     // 通常情况下我们只需要使用"err"
6.     result, err := doSomething()
7.     // 但是你也可以申明一个新的长名字变量,例如 "somethingError". 

8.     // Error作为后缀

9.     var specificError error
10.     result, specificError = doSpecificThing()
11.     // ... 后面就使用specificError.
12. }


不好的例子:
1. var ErrorSomething = errors.New("something went wrong")
2. var SomethingErr = errors.New("something went wrong")
3. func main() {
4.     var specificErr error
5.     result, specificErr = doSpecificThing()
6.     var errSpecific error
7.     result, errSpecific = doSpecificThing()
8.     var errorSpecific error
9.     result, errorSpecific = doSpecificThing()
10. }


【规则3.30】公共包内禁止使用panic,如果有panic需要内部recover并返回error。
说明:只有当实在不可运行的情况采用panic,例如文件无法打开,数据库无法连接导致程序无法正常运行,但是对于其他的package对外的接口不能有panic。


4 质量保证
4.1 代码质量保证优先原则
【原则4.1】代码质量保证优先原则:
(1)正确性,指程序要实现设计要求的功能。
(2)简洁性,指程序易于理解并且易于实现。
(3)可维护性,指程序被修改的能力,包括纠错、改进、新需求或功能规格变化的适应能力。
(4)可靠性,指程序在给定时间间隔和环境条件下,按设计要求成功运行程序的概率。
(5)代码可测试性,指软件发现故障并隔离、定位故障的能力,以及在一定的时间和成本前提下,进行测试设计、测试执行的能力。
(6)代码性能高效,指是尽可能少地占用系统资源,包括内存和执行时间。
(7)可移植性,指为了在原来设计的特定环境之外运行,对系统进行修改的能力。


4.2 对外接口原则
【原则4.2】对于主要功能模块抽象模块接口,通过interface提供对外功能。


说明:Go语言其中一个特殊的功能就是interface,它让面向对象,内容组织实现非常的方便。正确的使用这个特性可以使模块的可测试性和可维护性得到很大的提升。对于主要功能包(模块),在package包主文件中通过interface对外提供功能。


示例:在buffer包的buffer.go中定义如下内容
1. package buffer
2.
3. import (
4.     "policy_engine/models"
5. )
6.
7. //other code …
8. type MetricsBuffer interface {
9.     Store(metric *DataPoint) error
10.     Get(dataRange models.MatchPolicyDataRange) (*MetricDataBuf, error)
11.     Clear(redisKey string) error
12.     Stop()
13.     Stats() []MetrisBufferStat
14.     GetByKey(metricKey string) []DataPoint
15. }


使用buffer package的代码示例,通过interface定义,可以在不影响调用者使用的情况下替换package。基于这个特性,在测试过程中,也可以通过实现符合interface要求的类来打桩实现测试目的。
1. package metrics
2.
3. import (
4. ...//other import
5.     "policy_engine/worker/metrics/buffer"
6. )
7.
8. type MetricsClient struct {
9.     logger            lager.Logger
10.     redisClient       *store.RedisClient
11.     conf              *config.Config
12.     metricsBuffer     buffer.MetricsBuffer //interface类型定义的成员
13.     metricsStatClient *metricstat.MetricsStatClient
14.     stopSignal        chan struct{}
15. }
16.
17. func New(workerId string, redisClient *store.RedisClient, logger lager.Logger, conf *config.Config) *MetricsClient {
18.     var metricsBuffer MetricsBuffer
19.     if conf.MetricsBufferConfig.StoreType == config.METRICS_MEM_STORE {
20.         //具有interface定义函数的package实现,通过内存保存数据
21.         metricsBuffer = NewMemBuffer(logger, conf)  
22.     } else if conf.MetricsBufferConfig.StoreType == config.METRICS_REDIS_STORE {
23.         //具有interface定义函数的package实现,通过redis保存数据
24.         metricsBuffer = NewRedisBuffer(redisClient, logger, conf) 
25.     } else {
26.       ... //other code
27.     }
28.     ... //other code
29. }


4.3 值与指针(T/*T)的使用原则
关于接收者对指针和值的规则是这样的,值方法可以在指针和值上进行调用,而指针方法只能在指针上调用。这是因为指针方法可以修改接收者;使用拷贝的值来调用它们,将会导致那些修改会被丢弃。


对于使用T还是*T作为接收者,下面是一些建议:
【建议4.1】基本类型传递时,尽量使用值传递。
【建议4.2】如果传递字符串或者接口对象时,建议直接实例传递而不是指针传递。
【建议4.3】如果是map、func、chan,那么直接用T。
【建议4.4】如果是slice,method里面不重新reslice之类的就用T。
【建议4.5】如果想通过method改变里面的属性,那么请使用*T。
【建议4.6】如果是struct,并且里面包含了sync.Mutex之类的同步原语,那么请使用*T,避免copy。
【建议4.7】如果是一个大型的struct或者array,那么使用*T会比较轻量,效率更高。
【建议4.8】如果是struct、slice、array里面的元素是一个指针类型,然后调用函数又会改变这个数据,那么对于读者来说采用*T比较容易懂。
【建议4.9】其它情况下,建议采用*T。
参考:https://github.com/golang/go/wiki/CodeReviewComments#pass-values


4.4 init的使用原则

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值