https://golang.google.cn/doc/effective_go
代码格式化
type T struct {
name string // name of the object
value int // its value
}
// 使用gofmt自动格式化
type T struct {
name string // name of the object
value int // its value
}
格式说明:
缩进:gofmt默认使用制表符进行缩进,仅在必要时使用空格。
行长:没有一行代码长读的限制,如果自己觉得过长,可以使用制表符将一行代码分成多行。
括号:相比于C和Java,使用括号的地方更少,比如(if, for, switch)都不需要括号,而且,运算符优先级层次更短更清晰,比如:
x<<8 + y<<16
用行间距区分每一块。
注释
先引出go doc命令:
外部查看:
go build -o xx
go doc -all xx
或者直接指定文件
go doc -all xx.go
可以使用 /* */ 块注释和 // 行注释
包注释:唯一需要注意的地方是注释和package之间不能空行
例:
以下是合法的
/*
this is a package
*/
package a
以下是不合法的
/*
this is a package
*/
package a
如果包比较简单,也可以使用 // 注释。
函数注释:
// Test is a good test
func Test() {
fmt.Println("1")
}
函数注释的第一个单词最好是函数名,比如此处定义的函数名为Test,注释的第一个单词为Test
分组注释:
Go的声明语法允许对声明进行分组,单个文档注释可以引入一组相关的常量或变量。
// Error codes returned by failures to parse an expression.
var (
ErrInternal = errors.New("regexp: internal error")
ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
...
)
// Lock comment
var (
countLock sync.Mutex
inputCount uint32
outputCount uint32
errorCount uint32
)
注意:
一个组内至少有一个变量是大写,go doc才能获取到注释信息,如上lock中没有大写的变量,go doc不会输出Lock comment
函数注释时函数名首字母也需要大写,可以grep查看具体某个函数的注释
go doc -all xx |grep Test
包名
包名应该是简短,简洁,见名知意的,通常包名都为小写,不需要下划线或首字母大写;
不要使用这种import .表示法,它可以简化必须在其测试的程序包之外运行的测试,但应避免这样做;
另一个简短的例子是once.Do; once.Do(setup)阅读很好,不要写为once.DoOrWaitUntilDone(setup)。长名不会更具可读性。有用的文档注释通常比加长名称更有价值,也就是函数名不用写很长,用注释来说明。
Get/Set方法
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
不用写为owner := obj.GetOwer()
接口名称
按照惯例,一个方法接口由该方法name加上后缀-er或类似的修改命名构建的试剂名:Reader, Writer,Formatter, CloseNotifier等;
Read,Write,Close,Flush, String等有规范签名和意义,方法名尽量避免使用此类名称。
多字名称
使用大驼峰(如MixCaps)或小驼峰(如mixCaps)命名法,不使用下划线
分号
与C一样,Go的形式语法使用分号来终止语句,但是与C中不同,这些分号几乎不会出现在源代码中。相反,词法分析器使用一条简单的规则在扫描时自动插入分号,因此输入文本中几乎没有分号,仅在for循环子句之类的地方使用分号,以分隔初始化程序,条件和延续元素。
如果一行代码的末尾以以下结尾
break continue fallthrough return ++ -- ) }
语法解析器会自动在其后加分号
如下合法:
if i < f() {
g()
}
如下不合法:
if i < f() // wrong!
{ // wrong!
g()
}
会变成
if i<f();{g()}
if
鼓励在多行上编写简单的语句
// 不推荐
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
// 推荐
f, err := os.Open(name)
if err != nil {
return err
}
codeUsing(f)
省略不必要的else
// 没必要加else
f, err := os.Open(name)
if err != nil {
return err
}else{
codeUsing(f)
}
重新声明和重新赋值
f,err:= os.Open(name)
该语句声明了两个变量f和err。几行后,对f.Statread 的调用
d,err:= f.Stat()
看起来好像在声明d和err。但是请注意,err这两个语句中都出现了。这种重复是合法的:err由第一个语句声明,但仅在第二个语句中重新分配。这意味着对的调用将f.Stat使用err上面声明的现有 变量,并为其赋予一个新值。
for
go语言没有while和do while,都用for来模拟
// Like a C for
for init; condition; post { }
// Like a C while
for condition { }
// Like a C for(;;)
for { }
switch
两种模式:
// 那个条件为true执行哪步
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
// case中哪个值与c相等执行哪步
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
跳出顶层循环:会打印0 1 2
func Test() {
Loop:
for n := 0; n < 5; n++ {
fmt.Println(n)
switch {
case n==0:
break
case n==2:
break Loop
}
}
}
类型判断
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
多返回值
例:
func (file *File) Write(b []byte) (n int, err error)
func nextInt(b []byte, pos int) (value, nextPos int)
defer
了解return概念,执行return不等于函数退出,defer语句即在return执行后,函数退出前执行,defer可以修改返回值,但不建议这么做
先判错再defer
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close will run when we're finished.
后入先出模型,LIFO,栈模型,压栈时已经将数据都计算好了
func Test() (a int) {
n := 2
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i*n)
}
n = 3
defer func() {
a = 20
}()
a = 10
return
}
会打印:
8,6,4,2,0
返回值为20
分配内存
go有两种分配内存的方式,一种是new,一种是make,它们分别适用于不同的类型,new是一个分配内存的内置函数,但是与其他语言中的同名函数不同,它不会初始化内存,只会将其清零。new(T)会返回一个T类型零值的指针。
使用new分配内存:
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
new个人使用较少,一般如果需要返回某种类型的指针类型,使用&显示声明
p := &SyncedBuffer{}
使用make分配内存:
var p *[]int = new([]int) // allocates slice structure; *p == nil; 用得很少
var v []int = make([]int, 100) // 切片v现在引用一个包含100个整数的新数组
// 没必要分两步:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// 一步到位:
v := make([]int, 100)
make仅支持对chan, slice,map类型变量分配内存,make不返回指针,如果要指针类型的话用取地址符&
func Test() {
a := make(chan bool, 1)
fmt.Printf("%T\n", &a)
}
数组
1. 数组是值类型,拷贝等于Ctrl+c,Ctrl+v
2. 函数传递数组会产生拷贝操作,操作的对象不是原来的数组
3. 数组的容量是数组类型的一部分,[10]int和[20]int类型不一样
4. 数组没有append操作
类C操作可显示传指针
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
即使这样也不是惯用的Go,所以改用切片Slices
切片
切片包含对基础数组的引用,如果将一个切片分配给另一个切片,则两个切片都引用同一数组
函数传参:
func main(){
a := []int{}
fmt.Printf("%p\n", &a) // 0xc00000c060
Test(a)
fmt.Println(a) // []
}
func Test(a int[]){
fmt.Printf("%p\n", &a) // 0xc00000c080, 发生了拷贝,不再是原来的切片
for i:=0; i<1026; i++{
fmt.Println(cap(a)) // 1024之前超出容量,新的数组容量为原来容量的2倍,超过1024之后每次扩容当前容量的1/4
a = append(a, 1) // append操作会自动初始化,自动扩容
fmt.Printf("%p\n", &a) // 0xc00000c080,只是该切片的底层数组在变化,该切片不变
}
fmt.Println(a) // [1,1,1,,,1,1] 1026个
}
另一函数传参:
func main(){
a := []int{}
b := []int{1}
}
func Test(a, b []int){
a[0] = 10 // 未初始化
b[0] = 10 // ok,原切片b[0]也会改变,如果在重新赋值之前有append操作,原切片不变:
// b=append(b, 10), b[0]=10,此时修改的已不是原来的数组
// func append(slice []T, elements ...T) []T
// 修改完切片后应该返回修改后的切片,因为底层数组可能会改变
b[1] = 10 // panic越界
}
二维数组
func Test(){
var c [][]int
for i:=0; i<10; i++{
c = append(c, []int{i})
}
fmt.Println(c)
}
字典
1. 只要是能判等的类型,都能作为字典的key;
2. 切片不能作为字典的key, 字典不能作为字典的key
3. 整数,浮点数和复数,字符串,指针,接口(只要动态类型支持相等),结构体和数组可以作为字典的key
func Test(){
a, b := []int{1}, []int{2}
fmt.Println(a == b) // 不合法,不能判等
e, f := make(map[int]int), make(map[int]int)
fmt.Println(e == f) // 不能判等
}
判断key值存在:
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Println("unknown time zone:", tz)
return 0
}
如果只需判断是否存在:
_, ok := timeZone[tz]
删除key:
delete(timeZone, “tz”)
打印
以下输出的是相同的
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))
‘v’占位符
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
type T struct {
a int
b float64
c string
}
func main(){
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t) // &{7 -2.35 abc def}
fmt.Printf("%+v\n", t) //&{a:7 b:-2.35 c:abc def}
fmt.Printf("%#v\n", t) // &main.T{a:7, b:-2.35, c:"abc\tdef"}
fmt.Printf("%#v\n", timeZone) // map[string]int{"CST":-21600, "EST":-18000, //"MST":-25200, "PST":-28800, "UTC":0}
// 既可直接传字符串,也可传带占位符的
s := getLine("aab")
fmt.Println(s)
s = getLine("a %d", 10)
fmt.Println(s)
}
func getLine(format string, v ...interface{}) string {
s := fmt.Sprintf(format, v...)
return s
}
常量
1. 常量只能是数字、字符(符文)、字符串或布尔值
2. 由于编译时的限制, 定义它们的表达式必须也是可被编译器求值的常量表达式。例如 1<<3 就是一个常量表达式,而 math.Sin(math.Pi/4) 则不是,因为对 math.Sin 的函数调用在运行时才会发生
枚举常量使用枚举器 iota 创建,iota只能在常量的表达式中使用
fmt.Println(iota) // 报错
每次const出现,都会初始化iota为0
const中每新增一行常量声明iota加1
const (
a = iota // 0
b // 1
)
const (
c = iota // 0
d // 1
)
const(
_ = iota
e // 1
f // 2
)
变量
变量能像常量一样初始化,而且可以初始化为一个可在运行时得出结果的普通表达式
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
init函数
1. 每个源文件都可以通过定义自己的无参数 init 函数来设置一些必要的状态。 (每个文件都可以拥有多个 init 函数)而它的结束就意味着初始化结束: 只有该包中的所有变量声明都通过它们的初始化器求值后 init 才会被调用, 而包中的变量只有在所有已导入的包都被初始化后才会被求值。
2. 除了那些不能被表示成声明的初始化外,init 函数还常被用在程序真正开始执行前,检验或校正程序的状态。
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath 可通过命令行中的 --gopath 标记覆盖掉。
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}
3. 先执行导入的包中的init函数,再执行本包中的init函数
方法
指针 vs. 值
需要做到类型统一,要么都使用指针,要么都不使用指针
type ByteSlice []byte
func (slice ByteSlice) append1(data []byte) []byte {
// 主体与上面定义的Append函数完全相同。
}
// 可改为,不需要返回值,通过指针修改原切片
func (p *ByteSlice) append2(data []byte) {
//n, _ := (*p).Write([]byte("1"))
n, _ := p.Write([]byte("1")) // 语法糖
fmt.Println(n)
}
func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
// 同上。
*p = slice
return len(data), nil
}