优化一个已有的 Go 程序,提高其性能并减少资源占用|青训营

67 阅读5分钟

Go代码评估工具:

  • [goreporter] – 生成Go代码质量评估报告
  • [dingo-hunter]– 用于在Go程序中找出deadlocks的静态分析器
  • [flen]– 在Go程序包中获取函数长度信息
  • [go/ast]– Package ast声明了关于Go程序包用于表示语法树的类型
  • [gocyclo] – 在Go源代码中测算cyclomatic函数复杂性
  • [Go Meta Linter] – 同时Go lint工具且工具的输出标准化
  • [go vet] – 检测Go源代码并报告可疑的构造
  • [ineffassign] – 在Go代码中检测无效赋值
  • [safesql]– Golang静态分析工具,防止SQL注入

然后是如何在运行过程来调试Go程序,Go自带了一个pprof工具,这个工具可以做CPU和内存的profiling。

package main
import
   (
    "log"
    "net/http"
   _"net/http/pprof"
   )
func main()  {
      go func() {
        //port is you coustom define.
        log.Println(http.ListenAndServe("localhost:7000", nil))
      }()
      //do your stuff.
}

只需要引入net/http 和 _"net/http/pprof"即可,然后配合工具生成流程图,占比图清晰明了。

或者对于一些程序你还可以在运行时去改变它,调试它,使用[google/gops] 谷歌出品,你可以去查看栈,内存,CPU,heap等等信息,很不错,但是我不喜欢它开启了服务端口,这个项目刚开始是不需要使用新的端口,直接使用套接字文件通信,但是因为无法在windows上实现,最后作罢,从此好感降低了!

当然你还可以使用GDB工具,最新的GDB貌似还加入了查看goruntine的命令,很棒!

6.算法与优化思路

这个不用多说,说实话个人觉得算法是区分工程师和码农的一个很大分界点,算法可以说是基本能力,很多人不以为然觉得只是面试门槛,但是看看代码实现中数据结构的设计和算法实现就明白了!当遇到问题会不自觉的想到一个算法,这个目的就够了,其实并没有说算法非常牛逼,其实之前老司机聊天也说过只要你能在遇到问题能够想到用什么算法解决即可。

各种排序,集合操作,查询等等,没有最好的算法,只要最适合的算法

至于哪些方面需要优化,一方面是算法的效率还要就是现象,例如CPU特别高那么看看goruntine的调度,哪个函数占用比高,是不是存在死循环;内存大,看看是不是有大的内存分配没有及时回收,或者是不是有频繁的内存分配,是不是有内存泄露?响应慢是卡在哪里,是执行效率还是和组件通信等等。

7.Go的陷阱与技巧

a.make的陷阱

func main() {
	s := make([]int, 3)
	s = append(s, 1, 2, 3)
	fmt.Println(s)
}
结果
[0 0 0 1 2 3]

b.map读写冲突,产生竞态

c.文件打开,数据库连接记得一定要关闭或释放,一般使用defer

d.对于一个struct值的map,你无法更新单个的struct值

e.简化range

for range m {
}

f.defer的陷阱

有名返回值则是在函数声明的同时就已经被声明,匿名返回值是在return执行时被声明,所以在defer语句中只能访问有名返回值,而不能直接访问匿名返回值。

package main

import (
	"fmt"
)

func main() {
	fmt.Println("return:", defer_call())
}

func defer_call() int {
	var i int
	defer func() {
		i++
		fmt.Println("defer1:", i)
	}()
	defer func() {
		i++
		fmt.Println("defer2:", i)
	}()
	return i
}

defer2: 1
defer1: 2
return: 0

Q2.

package main

import (
	"fmt"
)

func main() {
	fmt.Println("return:", defer_call())
}

func defer_call() (i int) {
	defer func() {
		i++
		fmt.Println("defer1:", i)
	}()
	defer func() {
		i++
		fmt.Println("defer2:", i)
	}()
	return i
}

defer2: 1
defer1: 2
return: 2

g.短式变量声明的陷阱

那些使用过动态语言的开发者而言对于短式变量声明的语法很熟悉,所以很容易让人把它当成一个正常的分配操作。这个错误,将不会出现编译错误,但将不会达到你预期的效果。

package main
import "fmt"
func main() {  
    value := 1
    fmt.Println(value)     // prints 1
    {
        fmt.Println(value) // prints 1
        value := 2
        fmt.Println(value) // prints 2
    }
    fmt.Println(value)     // prints 1 (bad if you need 2)
}

这个说到底是代码边界和变量影响范围问题。

h.nil和显式类型

nil标志符用于表示interface、函数、maps、slices和channels的“零值”。如果你不指定变量的类型,编译器将无法编译你的代码,因为它不知道具体的类型,同时你也不能给string赋nil值。

package main

func main() {
    var value1 = nil // error
    _ = value1
    var value2 string = nil // error
    if value2 == nil { // error
        value2 = "test"
    }
}

应该

package main

func main() {
	var value1 interface{} = nil // error
	_ = value1
	var value2 string
	if value2 == "" {
		value2 = "test"
	}
}

i.全部是值传递,没有引用传递

如果你是一个C或则C++开发者,那么知道数组就是指针。当你向函数中传递数组时,函数会参照相同的内存区域,这样它们就可以修改原始的数据。但Go中的数组是数值,因此当你向函数中传递数组时,函数会得到原始数组数据的一份复制。如果你打算更新数组的数据,你将会失败。

j.select下的所有case遍历是随机的,在使用的过程中要注意,这和switch是不同的

l.使用接口实现一个类型分类函数:

func classifier(items ...interface{}) {
    for i, x := range items {
        switch x.(type) {
        case bool:
            fmt.Printf("param #%d is a bool\n", i)
        case float64:
            fmt.Printf("param #%d is a float64\n", i)
        case int, int64:
            fmt.Printf("param #%d is an int\n", i)
        case nil:
            fmt.Printf("param #%d is nil\n", i)
        case string:
            fmt.Printf("param #%d is a string\n", i)
        default:
            fmt.Printf("param #%d’s type is unknown\n", i)
        }
    }
}

l.Map值在获取的时候是无序的,所以当我们需要有序时就需要通过字符串数组排序间接得到

package main

import (
    "fmt"
    "sort"
)

func main() {
    var m = map[string]int{
        "unix":         0,
        "python":       1,
        "go":           2,
        "javascript":   3,
        "testing":      4,
        "philosophy":   5,
        "startups":     6,
        "productivity": 7,
        "hn":           8,
        "reddit":       9,
        "C++":          10,
    }
    var keys []string
    for k := range m {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    for _, k := range keys {
        fmt.Println("Key:", k, "Value:", m[k])
    }
}

m.init函数

开发过程我们经常会遇到主要逻辑开始前要声明或者一些全局的变量或者初始化操作,或者有时候我们仅仅需要import一些包,并不需要使用里面的函数,那就需要使用init初始化函数,一个package中可以有多个init,比如你在demo/A.go,demo/B.go都有一个init那么它们都会执行。

n.Go程序显示占用内存有时候并不是真正在用的内存,只是还没还给操作系统