Go分布式爬虫笔记(九)

09_Go编码规范

缩写说明

F: 强制

工具

  • golangci-lint

    • 静态代码分析工具,词语 Linter 指的是一种分析源代码以此标记编程错误、代码缺陷、风格错误的工具。

    • 集合多种 Linter 的工具。要查看支持的 Linter 列表以及启用 / 禁用了哪些 Linter,可以使用下面的命令:

      golangci-lint help linters
      
    • Go 语言定义了实现 Linter 的 API,它还提供了 golint 工具,用于集成了几种常见的 Linter。在源码中,我们可以查看怎么在标准库中实现典型的 Linter。

      • Linter 的实现原理是静态扫描代码的 AST(抽象语法树),Linter 的标准化意味着我们可以灵活实现自己的 Linters。不过 golangci-lint 里面其实已经集成了包括 golint 在内的总多 Linter,并且有灵活的配置能力。所以在自己写 Linter 之前,建议先了解 golangci-lint 现有的能力。
    • 在大型项目中刚开始使用 golang-lint 会出现大量的错误,这种情况下我们只希望扫描增量的代码。如下所示,可以通过在golangci-lint 配置文件中调整 new-from-rev 参数,配置以当前基准分支为基础实现增量扫描

      linters:
       enable-all: true
      issues:
       new-from-rev: master
      
  • Pre-Commit

    • 在代码通过 Git Commit 提交到代码仓库之前,git 提供了一种 pre-commit 的 hook 能力,用于执行一些前置脚本。在脚本中加入检查的代码,就可以在本地拦截住一些不符合规范的代码,避免频繁触发 CI 或者浪费时间。pre-commit 的配置和使用方法,可以参考TiDB
  • race

    • Go 1.1 提供了强大的检查工具 race 来排查数据争用问题。race 可以用在多个 Go 指令中,一旦检测器在程序中找到数据争用,就会打印报告。这份报告包含发生 race 冲突的协程栈,以及此时正在运行的协程栈。可以在编译时和运行时执行 race,方法如下:

      $ go test -race mypkg
      $ go run -race mysrc.go
      $ go build -race mycmd
      $ go install -race mypkg
      

      在下面这个例子中, 运行中加入 race 检查后直接报错。从报错后输出的栈帧信息中,我们能看出具体发生并发冲突的位置。

      » go run -race 2_race.go
      ==================
      WARNING: DATA RACE
      Read at 0x00000115c1f8 by goroutine 7:
        main.add()
          bookcode/concurrence_control/2_race.go:5 +0x3a
      Previous write at 0x00000115c1f8 by goroutine 6:
        main.add()
          bookcode/concurrence_control/2_race.go:5 +0x56
      

      第四行 Read at 表明读取发生在 2_race.go 文件的第 5 行,而第七行 Previous write 表明前一个写入也发生在 2_race.go 文件的第 5 行。这样我们就可以非常快速地定位数据争用问题了。
      竞争检测的成本因程序而异。对于典型的程序,内存使用量可能增加 5~10 倍,执行时间会增加 2~20 倍。同时,竞争检测器会为当前每个 defer 和 recover 语句额外分配 8 字节。在 Goroutine 退出前,这些额外分配的字节不会被回收。这意味着,如果有一个长期运行的 Goroutine,而且定期有 defer 和 recover 调用,那么程序内存的使用量可能无限增长。(这些内存分配不会显示到 runtime.ReadMemStats 或 runtime / pprof 的输出。)

  • 覆盖率

    • 一般我们会使用代码覆盖率来判断代码书写的质量,识别无效代码。go tool cover 是 go 语言提供的识别代码覆盖率的工具,

我们为什么需要编程规范?

好处:

  • 促进团队合作
  • 规避错误
  • 提升性能
  • 便于维护

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

任何傻瓜都可以编写电脑能理解的代码。好的程序员编写的是人类可以理解的代码。

  • 整洁

    • 格式化

      • 代码长度

        • F(行 120)

        • F(函数 40)

          • swich?
        • F(文件 2000)

      • 代码布局

        • 包注释: 对整个模块和功能的完整描述,写在文件头部。

        • Package:包名称。

        • Imports: 引入的包。

        • [F goimports] 当 import 多个包时,应该对包进行分组。同一组的包之间不需要有空行,不同组之间的包需要一个空行。标准库的包应该放在第一组。

          import (
              "fmt"
              "hash/adler32"
              "os"
           
              "appengine/foo"
              "appengine/user"
              "github.com/foo/bar"
              "rsc.io/goversion/version"
          )
          
        • Constants:常量定义。

        • Typedefs:类型定义。

        • Globals:全局变量定义。

        • Functions:函数实现。

        • 每个部分之间用一个空行分割。每个部分有多个类型定义或者有多个函数时,也用一个空行分割。

        • 示例如下:

          /*
          注释
          */
          package http
            
          import (
           "fmt"
           "time"
          )
            
          const (
           VERSION = "1.0.0"
          )
          
          type Request struct{
          }
            
          var msg = "HTTP success"
            
          func foo() {
           //...
          }
          
      • 空格与缩进

        • [F gofmt] 注释和声明应该对齐

        • [F gofmt] 小括号 ()、中括号[]、大括号{} 内侧都不加空格。

        • [F gofmt] 逗号、冒号(slice 中冒号除外)前都不加空格,后面加 1 个空格。

        • [F gofmt] 所有二元运算符前后各加一个空格,作为函数参数时除外。例如b := 1 + 2。

        • [F gofmt] 使用 Tab 而不是空格进行缩进。

        • [F nlreturn] return 前方需要加一个空行,让代码逻辑更清晰。

        • [F gofmt] 判断语句、for 语句需要缩进 1 个 Tab,并且右大括号}与对应的 if 关键字垂直对齐。例如:

          if xxx {
            
          } else {
          
          }
          
        • [推荐] 避免 else 语句中处理错误返回,避免正常的逻辑位于缩进中。如下代码实例,else 中进行错误处理,代码逻辑阅读起来比较费劲。

          if something.OK() {
                  something.Lock()
                  defer something.Unlock()
                  err := something.Do()
                  if err == nil {
                          stop := StartTimer()
                          defer stop()
                          log.Println("working...")
                          doWork(something)
                          <-something.Done() // wait for it
                          log.Println("finished")
                          return nil
                  } else {
                          return err
                  }
          } else {
                  return errors.New("something not ok")
          }
          

          如果把上面的代码修改成下面这样会更加清晰:

          if !something.OK() {  
              return errors.New("something not ok")
          }
          something.Lock()
          defer something.Unlock()
          err := something.Do()
          if err != nil {   
              return err
          }
          stop := StartTimer()
          defer stop()
          log.Println("working...")
          doWork(something)
          <-something.Done() // wait for it
          log.Println("finished")
          return nil
          
        • [推荐] 函数内不同的业务逻辑处理建议用单个空行加以分割。

        • [推荐] 注释之前的空行通常有助于提高可读性——新注释的引入表明新思想的开始。

    • 命名

      • 短,容易拼写;

      • 保持一致性;

      • 意思准确,容易理解,没有虚假和无意义的信息。

      • [F revive] Go 中的命名统一使用驼峰式、不要加下划线

      • [F revive] 缩写的专有名词应该大写,例如: ServeHTTP、IDProcessor。

      • [强制] 区分变量名应该用有意义的名字,而不是使用阿拉伯数字:a1, a2, … aN。

      • [强制] 不要在变量名称中包含你的类型名称。

      • [建议]变量的作用域越大,名字应该越长

      • 包名

        • 简短而清晰

        • [强制] 使用简短的小写字母,不需要下划线或混合大写字母

        • [建议] 合理使用缩写,例如:

          strconv(字符串转换)
          syscall(系统调用)
          fmt(格式化的 I/O)
          
        • [强制] 避免无意义的包名,例如 util,common,base 等。
          感觉经常有些通用的

      • 接口命名

        • 单方法接口由方法名称加上 -er 后缀或类似修饰来命名。例如:Reader, Writer, Formatter, CloseNotifier
        • 当一个接口包含多个方法时,请选择一个能够准确描述其用途的名称(例如:net.Conn、http.ResponseWriter、io.ReadWriter)
      • 变量命名

        • [建议]尽可能地短。在这里,i 指代 index,r 指代 reader,b 指代 buffer。
      • 函数命名

        • [建议]如果函数参数的类型已经能够看出参数的含义,那么函数参数的命名应该尽量简短:

          func AfterFunc(d Duration, f func()) *Timer
          func Escape(w io.Writer, s []byte)
          
        • [建议]如果函数参数的类型不能表达参数的含义,那么函数参数的命名应该尽量准确:

          func Unix(sec, nsec int64) Time
          func HasPrefix(s, prefix []byte) bool
          
        • [建议] 对于公开的函数,返回值具有文档意义,应该准确表达含义,如下所示:

          func Copy(dst Writer, src Reader) (written int64, err error)
          
          func ScanBytes(data []byte, atEOF bool) (advance int, token []byte, err error)
          
      • 可导出的变量名: 由于使用可导出的变量时会带上它所在的包名,因此,不需要对变量重复命名。例如 bytes 包中的 ByteBuffer 替换为 Buffer,这样在使用时就是 bytes.Buffer,显得更简洁。类似的还有把 strings.StringReader 修改为 strings.Reader,把 errors.NewError 修改为 errors.New。

      • Error 值命名

        • [建议] 错误类型应该以 Error 结尾。

        • [建议] Error 变量名应该以 Err 开头。

          type ExitError struct {
              ...
          }
          var ErrFormat = errors.New("image: unknown format")
          
    • 函数

      • [强制 cyclop] 圈复杂度(Cyclomatic complexity)<10。

      • [强制 gochecknoinits] 避免使用 init 函数

      • [强制 revive] Context 应该作为函数的第一个参数

      • [强制] 正常情况下禁用 unsafe。

      • [强制]​ 禁止 return 裸返回,如下例中第一个 return:

        func (f *Filter) Open(name string) (file File, err error) {
          for _, c := range f.chain {
            file, err = c.Open(name)
            if err ! =  nil {
              return
            }
          }
          return f.source.Open(name)
        }
        
      • [强制]​ 不要在循环里面使用 defer,除非你真的确定 defer 的工作流程。

      • [强制] 对于通过:= 进行变量赋值的场景,禁止出现仅部分变量初始化的情况。例如在下面这个例子中,f 函数返回的 res 是初始化的变量,但是函数返回的 err 其实复用了之前的 err:

        var err error
        res,err := f()
        
      • [建议] 函数返回值大于 3 个时,建议通过 struct 进行包装

      • [建议] 函数参数不建议超过 3 个,大于 3 个时建议通过 struct 进行包装。

    • 控制结构

      • [强制] 禁止使用 goto。
      • [强制 gosimple] 当一个表达式为 bool 类型时,应该使用 expr 或 !expr 判断,禁止使用 == 或 != 与 true / false 比较。
      • [强制 nestif] if 嵌套深度不大于 5
    • 方法

      • [强制 revive] receiver 的命名要保持一致,如果你在一个方法中将接收器命名为 “c”,那么在其他方法中不要把它命名为 “cl”。

      • [强制] receiver 的名字要尽量简短并有意义,禁止使用 this、self 等。

        
        func (c Client) done() error {
         // ...
        }
        func (cl Client) call() error {
         // ...
        }
        
    • 注释

      • [强制] 无用注释直接删除,无用的代码不应该注释而应该直接删除。即使日后需要,我们也可以通过 Git 快速找到。

      • [强制] 紧跟在代码之后的注释,使用 //。

      • [强制] 统一使用中文注释,中英文字符之间严格使用空格分隔

      • [强制] 注释不需要额外的格式,例如星号横幅。

      • [强制] 包、函数、方法和类型的注释说明都是一个完整的句子,以被描述的对象为主语开头。Go 源码中都是这样的。

        // queueForIdleConn queues w to receive the next idle connection for w.cm.
        // As an optimization hint to the caller, queueForIdleConn reports whether
        // it successfully delivered an already-idle connection.
        func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) 
        
      • [强制] Go 语言提供了文档注释工具 go doc,可以生成注释和导出函数的文档。文档注释的写法可以参考文稿中的链接。

      • [强制 godot] 注释最后应该以句号结尾

      • [建议] 当某个部分等待完成时,可用 TODO: 开头的注释来提醒维护人员。

      • [建议] 大部分情况下使用行注释块注释主要用在包的注释上,不过块注释在表达式中或禁用大量代码时很有用。

      • [建议] 当某个部分存在已知问题需要修复或改进时,可用 FIXME: 开头的注释来提醒维护人员

      • [建议] 需要特别说明某个问题时,可用 NOTE: 开头的注释。

    • 结构体

      • [强制] 不要将 Context 成员添加到 Struct 类型中。
  • 一致

  • 高效

    • [强制] Map 在初始化时需要指定长度make(map[T1]T2, hint)。

    • [强制] Slice 在初始化时需要指定长度和容量make([]T, length, capacity)。扩容性能损失

    • [强制] time.After() 在某些情况下会发生泄露,替换为使用 Timer。

    • [强制] 数字与字符串转换时,使用 strconv,而不是 fmt

    • [强制] 读写磁盘时,使用读写 buffer

    • [建议] 谨慎使用 Slice 的截断操作和 append 操作,除非你知道下面的代码输出什么:

      x := []int{1, 2, 3, 4}
      y := x[:2]
      fmt.Println(cap(x), cap(y))
      y = append(y, 30)
      fmt.Println("x:", x)
      fmt.Println("y:", y)
      
    • [建议] 任何书写的协程,都需要明确协程什么时候退出。

    • [建议] 热点代码中,内存分配复用内存可以使用 sync.Pool 提速

    • [建议] 将频繁的字符串拼接操作(+=),替换为 StringBuffer 或 StringBuilder。

    • [建议] 使用正则表达式重复匹配时,利用 Compile 提前编译提速

    • [建议] 当程序严重依赖 Map 时,Map 的 Key 使用 int 而不是 string 将提速

    • [建议] 多读少写的场景,使用读写锁而不是写锁将提速。

  • 健壮

    • [强制] 除非出现不可恢复的程序错误,否则不要使用 panic 来处理常规错误,使用 error 和多返回值。

    • [强制 revive] 错误信息不应该首字母大写(除专有名词和缩写词外),也不应该以标点符号结束。因为错误信息通常在其他上下文中被打印。

    • [强制 errcheck] 不要使用 _ 变量来丢弃 error。如果函数返回 error,应该强制检查。

    • [建议] 在处理错误时,如果我们逐层返回相同的错误,那么在最后日志打印时,我们并不知道代码中间的执行路径。例如找不到文件时打印的No such file or directory,这会减慢我们排查问题的速度。因此,在中间处理 err 时,需要使用 fmt.Errorf 或第三方包给错误添加额外的上下文信息。像下面这个例子,在 fmt.Errorf 中,除了实际报错的信息,还加上了授权错误信息authenticate failed :

      func AuthenticateRequest(r *Request) error {
              err := authenticate(r.User)
              if err != nil {
                      return fmt.Errorf("authenticate failed: %v", err)
              }
              return nil
      }
      

      有多个错误需要处理时,可以考虑将 fmt.Errorf 放入 defer 中:

      func DoSomeThings(val1 int, val2 string) (_ string, err error) {
          defer func() {
              if err != nil {
                  err = fmt.Errorf("in DoSomeThings: %w", err)
              }
          }()
          val3, err := doThing1(val1)
          if err != nil {
              return "", err
          }
          val4, err := doThing2(val2)
          if err != nil {
              return "", err
          }
          return doThing3(val3, val4)
      }
      
    • [强制] 利用 recover 捕获 panic 时,需要由 defer 函数直接调用。
      例如,下面例子中的 panic 是可以被捕获的:

      package main
      
      import "fmt"
      
      func printRecover() {
          r := recover()
          fmt.Println("Recovered:", r)
      }
      
      func main() {
          defer printRecover()
      
          panic("OMG!")
      }
      

      但是下面这个例子中的 panic 却不能被捕获:

      package main
      
      import "fmt"
      
      func printRecover() {
        r := recover()
        fmt.Println("Recovered:", r)
      }
      
      func main() {
        defer func() {
          printRecover()
        }()
      
        panic("OMG!")
      }
      
    • [强制] 不用重复使用 recover,只需要在每一个协程的最上层函数拦截即可。recover 只能够捕获当前协程,而不能跨协程捕获 panic,下例中的 panic 就是无法被捕获的.

    • [强制] 有些特殊的错误是 recover 不住的,例如 Map 的并发读写冲突。这种错误可以通过 race 工具来检查。

  • 可扩展

    • [建议] 利用接口实现扩展性。接口特别适用于访问外部组件的情况,例如访问数据库、访问下游服务。另外,接口可以方便我们进行功能测试。关于接口的最佳实践,需要单独论述。

    • [建议] 使用功能选项模式对一些公共 API 的构造函数进行扩展,大量第三方库例如 gomicro、zap 等都使用了这种策略。

      db.Open(addr, db.DefaultCache, zap.NewNop())
      可以替换为=>
      db.Open(
      addr,
      db.WithCache(false),
      db.WithLogger(log),
      )
      

参考

性能对比


「此文章为3月Day9学习笔记,内容来源于极客时间《Go分布式爬虫实战》,强烈推荐该课程!/推荐该课程」

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: 好的,下面是一个简单的 Go 语言爬虫程序示例: ``` package main import ( "fmt" "io/ioutil" "net/http" ) func main() { resp, err := http.Get("https://www.example.com") if err != nil { fmt.Println("Error:", err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println("Error:", err) return } fmt.Println(string(body)) } ``` 这段代码使用 `net/http` 包发送了一个 HTTP GET 请求到 `https://www.example.com`,并将响应的内容打印到了控制台。 如果你需要更复杂的爬虫程序,可以使用第三方包,例如 `colly` 和 `goquery`,它们提供了更多的功能和方便。 ### 回答2: 使用Go语言编写一个爬虫程序可以分为以下步骤: 1. 导入所需的Go库,例如"net/http"用于发送HTTP请求,"strings"用于处理字符串,"io/ioutil"用于读取HTTP响应的主体内容等。 2. 创建一个函数来发送HTTP请求并返回响应。在函数中,可以使用`http.Get()`方法来发送GET请求,并处理可能的错误。然后使用`ioutil.ReadAll()`方法将HTTP响应的主体内容读入一个变量中,并返回该变量。 3. 创建一个函数来解析HTTP响应中的HTML内容。使用第三方的HTML解析库(例如"germ.io/x/net/html")来解析HTML内容。可以使用该库的`NewTokenizer()`方法来创建一个HTML解析器,然后使用循环来依次解析HTML标签,并提取所需的信息。 4. 创建一个主函数,其中包括爬虫的主要逻辑。在主函数中,可以使用发送HTTP请求和解析HTML的函数来发起请求并提取信息。可以使用循环来处理多个页面,例如遍历整个网站的页面。 5. 最后,可以将提取的信息保存在文件中或进行进一步处理。 需要注意的是,在编写爬虫程序时,应遵守网站的服务条款和使用权限,并尊重网站的隐私政策。另外,为了避免对目标网站造成过大的负载,应添加适当的延时和限制请求的频率。 ### 回答3: 使用Go语言编写爬虫程序可以通过以下步骤实现: 1. 导入所需的包: 首先,在Go语言中,我们需要使用`net/http`包来发送HTTP请求,`io/ioutil`包来读取网页内容,`regexp`包来匹配网页中的数据。因此,我们需要导入这些包。 2. 发送HTTP请求: 通过`http.Get`函数发送HTTP GET请求获取网页内容,将其保存到一个响应(`http.Response`)对象中。 3. 读取网页内容: 使用`ioutil.ReadAll`函数从响应对象中读取整个网页的内容,并将其保存到一个字节切片(`[]byte`)中。 4. 解析网页内容: 使用正则表达式(`regexp`包)或者解析HTML(`goquery`包)等方法来获取我们需要的数据。通过在网页内容中匹配指定的模式或者使用CSS选择器来选择指定的HTML元素。 5. 处理获取的数据: 根据我们的需求,对获取的数据进行进一步的处理、过滤和存储。可能需要将数据保存到文件、数据库或者其他存储介质中。 例如,我们可以使用Go语言来编写一个简单的爬取网页标题的程序: ```go package main import ( "fmt" "io/ioutil" "net/http" "regexp" ) func main() { // 发送HTTP GET请求 response, err := http.Get("http://example.com") if err != nil { fmt.Println("Error: ", err) return } defer response.Body.Close() // 读取网页内容 body, err := ioutil.ReadAll(response.Body) if err != nil { fmt.Println("Error: ", err) return } // 解析网页内容,获取标题 titleRe := regexp.MustCompile(`<title>(.*?)</title>`) title := titleRe.FindStringSubmatch(string(body)) if len(title) > 1 { fmt.Println("Title: ", title[1]) } else { fmt.Println("Title not found") } } ``` 通过上述步骤,我们可以成功使用Go语言编写一个简单的爬虫程序,用于获取网页的标题。当然,实际的爬虫程序可能需要更加复杂的处理和更多的功能,这只是一个示例。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值