effective_go

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
}

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值