方法

  • Go语言中只有不同的package中可以有相同函数名的函数,尽管参数不同,但是如果函数名相同就不能出现在相同的包中(函数重载Go语言没有)
  • 方法需要通过一个特定的实例调用,比如t.Errorf(),这里的Errorf就是一个方法,通过实例t调用
  • 函数可以随便调用,没有限制

接口

  • 接口让函数接受不同类型的参数并创造类型安全并且高解耦的代码
  • Go语言中 interface resolution 是隐式的。如果传入的类型匹配接口需要的,则编译正确。
  • 函数实现因此不需要关心参数是什么类型的,只需要声明一个接口,辅助函数就可以从具体类型解耦而只关心本身需要做的工作

表格驱动测试

  • 如果需要测试一个接口的不同实现,或者传入的数据有很多不同的测试需求,则可以使用表格驱动测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func TestArea(t *testing.T){
areaTests := []struct{
name string
shape Shape
want float64
}{
{name: "Rec", shape: Rec{Width: 12, Height: 6}, want: 72.0},
{name: "Circle", shape: Circle{10}, want: 314.15926},
{name: "Tri", shape: Tri{12, 6}, want: 361.0},
}
for _, tt := range areaTests{
got := tt.shape.Area()
want := tt.want
if got != want{
t.Errorf("%#v got '%.2f', want '%.2f'", tt.shape, got, want)
}
}
}
  • 创建了一个匿名结构体,里面有shape和want,放在一个[]struct切片中
  • 然后使用两个测试用例填充这个切片
  • Go中调用一个函数或者方法的时候,参数会被复制
  • 使用指针解决这个问题,指向某个值,然后修改

map

  • Map是引用类型的,拥有对底层数据结构的引用
  • 因此,Map可以是nil指,如果使用一个nil的map,那么会得到一个nil指针异常,导致程序终止
  • 永远不要初始化一个空的map变量,比如:var m map[string]string
  • 可以用如下两种方式初始化空的map:
1
2
dict = map[string]string{}
dict = make(map[string]string)
  • 上述两种方法绝对不会出现nil指针异常

依赖注入

  • fmt.Fprintf接受一个Writer参数,将字符串传递过去。
  • fmt.Printf是标准的默认输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// di.go
package main

import (
"fmt"
"io"
"os"
)

func Greet(writer io.Writer, name string) {
// fmt.Printf("Hello, %s", name)
fmt.Fprintf(writer, "Hello, %s", name)
}

func main(){
Greet(os.Stdout, "Elodie")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// di_test.go
package main

import (
"bytes"
"testing"
)

func TestGreet(t *testing.T) {
buffer := bytes.Buffer{} // 注入依赖
Greet(&buffer, "Chris")
got := buffer.String()
want := "Hello, Chris"
if got != want {
t.Errorf("got %s want %s", got, want)
}
}
  • Go中可以使用反引号创建字符串,允许将字符串中的东西放在新的一行,比如
    `
    3
    1
    2
    `
  • Go中不会阻塞的操作在成为goroutine的单独进程中运行。使用go关键字声明

  • go test -race可以发现goroutine中的竞争条件,比如可能多个进程同时写一个map,但是一次执行并不会触发这种现象。

  • 可以通过channels协调goroutine解决数据竞争问题。

  • 比如原本需要将多个进程的数据写入map中,现在可以使用channel <- data,将数据发送到channel中,然后再使用for循环,将数据保存保存在新的map中,这样不会产生数据竞争的问题。result := <- channel

  • 在函数调用之前加上defer前缀会在包含他的函数结束时调用它。

    • 有时候需要清理资源,比如在函数结束时关闭一个文件,或者关闭一个服务器,但是要把它放在创建服务器语句附近,以便函数内后面的代码仍然可以使用这个服务器,就可以使用defer,等到函数执行完再调用

进程同步

  • select可以轻易实现进程同步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func Racer (a, b string) (winner string, err error){
select {
case <- ping(a):
return a, nil
case <- ping(b):
return b, nil
case M- time.After(10 *time.Second):
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
}
}

func ping(url string) chan bool{
ch := make (chan bool)
go func(){
http.Get(url)
ch <- true
}()
return ch
}
  • 如果是v := <- ch等待值发送给channel,则这是一个阻塞调用,因为需要等待值返回
  • select允许多个channel等待,第一个发送值的channel胜出。
  • 使用select时,time.After是一个很好用的函数,因为channel可能永远不会返回一个值,那就有可能不会返回,因此使用time.After设置超时时间
1
2
3
4
5
6
7
8
9
10
11
12
13
t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) {
serverA := makeDelayedServer(11 * time.Second)
serverB := makeDelayedServer(12 * time.Second)

defer serverA.Close()
defer serverB.Close()

_, err := Racer(serverA.URL, serverB.URL)

if err == nil {
t.Error("expected an error but didn't get one")
}
})

反射

编写函数 walk(x interface{}, fn func(string)),参数为结构体x,并对 x 中的所有字符串字段调用 fn 函数

  • 反射提供了程序检查自身结构体的能力。
  • 允许使用类型interface{},代表任意类型。但是这样市区了对类型安全的检查,编译器不会再检查类型
  • 除非真的需要,否则不要使用反射
  • 如果想要实现多态,可以考虑围绕接口实现。