实效Go编程简析

Effective Go是关于如何写出高效Go程序的文档。

分号


Go中的分号不在源码中出现,由词法分析器自动添加。当然If, For中自己写的。 规则是这样的:若在新行前的最后一个标记为标识符(包括 int 和 float64 这类的单词)、数值或字符串常量之类的基本字面或以下标记之一

break continue fallthrough return ++ -- ) }

so:

if i < f()  // 错!
{           // 该行行首会被增加一个分号
    g()
}

重新声明与重复赋值


Go中的短声明 := 的使用。

f, err := os.Open(name)

(some other code...)

d, err := f.Stat()

在上述的代码段中,err 在第一条语句中被声明,在第二次被重新赋值

以声明的变量v可以再次出现在 := 有几个条件:

  • 本次声明和已声明的err处于同一作用域。
  • 类型与之前的类型相应。
  • 在此次声明中至少有一个新声明的变量,如d.

For与Switch


注意,此处和C语言中的Goto不一样,标识符只能放在For或者Switch外面。 中间不能有其他语句,否则会报错。 执行效果是跳出当前的For或者Switch语句块,本次不会再重复进入。

如下所示,Here1,Here2分别为for和Switch工作,互不影响。

//第一段代码
Here1:
    for v := a[0]; v < 5; v++ {
        fmt.Println("for comming", v)
        if v == 3 {
            fmt.Printf("v==2 %+v \n", a)
            break Here1
        }
    Here2:
        switch {
        case m == 1:
            fmt.Printf("%+v \n", a)
            m = 2
            break Here2
        case m == 2:
            fmt.Printf("we get 2")
        }
        fmt.Printf("end : %+v \n", a)
    }
    return

switch还存在一个利用断言,动态判断接口类型的语法,如下

//第二段代码

    var t interface{}
    t = 8888
    switch v := t.(type) {
    default:
        fmt.Printf("unexpected type %T \n", t) // %T 输出 t 是什么类型
    case bool:
        fmt.Printf("boolean %t\n", t) // t 是 bool 类型
    case int:
        fmt.Printf("integer %d\n", t) // t 是 int 类型
        fmt.Printf("integer %d\n", v) // t 是 int 类型
    case *bool:
        fmt.Printf("pointer to boolean %t\n", *v) // t 是 *bool 类型
    case *int:
        fmt.Printf("pointer to integer %d\n", *v) // t 是 *int 类型
    }

可命名结果形参与Defer


  • Go函数的返回值或结果“形参”可被命名,并作为常规变量使用,就像传入的形参一样。
  • 命名后,一旦该函数开始执行,它们就会被初始化为与其类型相应的零值;
  • 若该函数执行了一条不带实参的 return 语句,则结果形参的当前值将被返回。

当可命名形参与Defer碰到一起,又会有不一样的问题。具体请看Go语言的defer,你真的懂了吗

由官网的例子,Defer函数的实参(如果该函数为方法则还包括接收者)在推迟执行时就会求值, 而不是在调用执行时才求值。所以,可以由这个特性来监控另一个函数的进入和退出。

New与Make


type test struct {             
    one int                    
    two bool                   
    three []int                
} 
  
func main() {                  
    first := new(test)         
    second := make([]test,1)   
    third := new([]bool)       
    fourth := make([]bool,1)   
    fifth := new(bool)
    sixth := make([]bool,1)
    fmt.Printf("%+v  \n%+v \n%+v \n%+v \n%+v \n%+v \n", *first, second, *third, fourth, *fifth, sixth)
}   

结果是:
{one:0 two:false three:[]}  //新声明的类型,除了切片都置零(bool类型的0就是false)。
[{one:0 two:false three:[]}]  //初始化了,0 和 false, 但是内层的切片未初始化。
[]         // 返回的是一个指向nil的空指针
[false]    // 返回的是初始化之后的 bool 切片
false      // 内置类型置零,就是false
[false]    // 返回初始化资源后的 test 切片

首先,明确的第一点:new返回的是指针,make返回的是object本身。

new: 可以对确定类型的一个对象置零,对slice, map, channel等无用。

make: 只能对slice, map, channel操作,初始化。

因此,针对官网的置零与初始化的差别:对于普通的类型,new和make都可以完成初始化,但是对于slice, channel, map这三个“本质上为引用数据类型”,只能使用make.

二维切片


和C语言的多维数组相同的原理,一共有两种初始化的方法:一次申请再重新分配和多次申请。

const (
    x = 2
    y = 4
)

func main() {
    picture := make([][]int, x)     // picture[x][y]。    
    /*
    test := make([]int,x * y)
    for i:= range picture {
        picture[i], test= test[:y], test[y:] //这招,太神奇。切片的magic.
    }
    */
    for i := range picture {
        picture[i] = make([]int, y)
    }
    fmt.Printf("%+v \n",picture)
}

输出都是:
[[0 0 0 0] [0 0 0 0]]

映射


没有定义相等的数据类型不能用作映射的键。

打印

有一个坑需要注意:

type MyString string

func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // 错误:会无限递归
}

如果以以 %f 调用了Sprintf,它并不是一种字符串格式:Sprintf 只会在它需要字符串时才调用 String 方法,而 %f 需要一个浮点数值。

枚举器 iota


iota可以做表达式的一部分,该表达式隐式重复,可以表达更复杂的枚举,比C更强大。

const (
    _ = iota  // 忽略第一个0
    x         // 即x = iota
    y         // 即y = iota
)

结果:x = 1, y = 2

指针与值


  • 值方法可通过指针和值调用,执行效果都是值方法。

  • 指针方法只能通过指针来调用。如果值是可寻址的,则编译器自动将值调用指针方法转为地址调用。执行效果是指针方法。

总的来说,编译器会帮助coder保持值调用或者指针调用的效果。

断言与接口类型


type Stringer interface {
    String() string
}

var value interface{} // 调用者提供的值。
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

//等价于以下的代码
if str, ok := value.(string); ok {   //不采用这种ok形式的话,如果断言失败,程序会崩。
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}

接口内嵌与结构体继承


Go中接口与继承的选择这部分的主要内容来自这里。

结论是:“为了不重写代码,必须使用继承。而为了存储抽象类型,调用具体类型的方法,必须用接口。”

const (
    PARENT      = iota
    CHILD
    GRANDCHILD
)

type object  int 

type ObjectInterface interface {       // object类的接口
    ObjectClass() int 
}

func (test *object) add() int {
        a := int(*test)
        a++ 
        *test = object(a)
        return a
}

func (test object) ObjectClass() int { // object类的方法
    return PARENT
}

type child struct {                    // child 继承 object
    object
}

func (my child) ObjectClass() int {    // child 实现Object 接口
    return CHILD
}

func (my child) add2() int {           // child 类的方法
    a := int(my.object)
    a++
    return a
}

type ChildInterface interface {        // child 接口
    ObjectInterface
    ChildClass() int
}

type grandchild struct {               // grandchild 继承 child
    child
}

func (my grandchild) ObjectClass()int{ // grand 继承 child 之后,是不用实现ObjectClass()
    return GRANDCHILD                  // 但是会返回child。因此单独实现。              
}

func (my grandchild) ChildClass()int { // 实现了child 接口。
    return GRANDCHILD
}

func main() {
    first   := object(0)
    second  := child{first}
    third   := grandchild{second}

    fmt.Printf("add: %d \n",third.add())
    fmt.Printf("add: %d \n",third.add())
    fmt.Printf("add: %d \n",third.add2())
    fmt.Printf("ObjectClass: %d \n",third.ObjectClass())

    if _, ok := interface{}(third).(ObjectInterface); ok {
        fmt.Printf("first have \n")
    }
    if _, ok := interface{}(third).(ChildInterface); ok {
        fmt.Printf("second have \n")
    }
}

输出是:
add: 1 
add: 2 
add: 3 
ObjectClass: 2 
first have 
second have
  • 未完待续