Golang 1:基础

本文总结了 Golang 的基本使用。

Hello World

package main
​
import "fmt"
​
func main() {
    fmt.Println("Hello, World")
}

语法基础

  1. Go 程序由 package 组成
  2. 入口在 main package
  3. 在程序中引入包的方式:
    a. "fmt"
    b. "math/rand"
  4. 按照惯例包的名称是包路径的最后一个元素,如 "manth/rand" 的名称为 "rand"
  5. Import
    import "fmt"
    import "math"

等价于
import (
   "fmt"
   "math"
)

  1. 在一个 package 中,以大写字母开头的属性或者函数是公开的;已小写字母开头的属性或者非法是私有的。

函数

  1. 定义
    func add(x int, y int) int {
       return x + y
    }

  2. 相邻参数的类型如果是一样的,可以只写一次类型
    func add(x, y int) int {
       return x + y
    }

  3. 函数可以有多个返回值
    func swap(x, y int) (int, int) {
       return y, x
    }

  4. 可以命名返回值,命名的参数会被当做定义在函数顶部的变量,提示返回值的含义,没有参数的 return 语句返回命名返回值变量,称为裸 return,只应该在短的函数使用,在长函数中不利于阅读。
    func split(sum int) (x, y int) {
       x = sum * 4 / 9
       y = sum - x
       return
    }

  5. 命名参数会被初始化为类型的零值

变量

  1. Var 声明 package 和 function 级别的变量:var x, y int
  2. Var 声明变量时可以指定初始值:var x, y int = 1, 2
  3. Var 声明变量时指定初始值可以省略类型:var x, y, z = 1, true, "jack"
  4. 函数内部可以用 := 声明变量,外部不行
  5. 变量类型
bool
string
​
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64
​
byte // alias for int8
rune // alias for int32
​
     // represents a Unicode code point
​
float32 float64
​
complex64 complex128
  1. 变量的零值:
  2. 数值型:0
  3. 布尔型:false
  4. 字符串型:""
  5. 类型转换:T(v)
  6. 类型推导:变量没有声明类型时,由赋予值时右边值的类型决定

常量

  1. 常量用 const 声明,不能用 :=
  2. 多个常量可以用 cont () 一次声明

循环结构

  1. 基本结构
package main
​
import "fmt"
​
func main() {
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }
}
  1. 初始和后置条件可以为空
package main
​
import "fmt"
​
func main() {
        sum := 1
        for ; sum < 1000; {
                sum += sum
        }
        fmt.Println(sum)
}
  1. 可以省略 ;,相当于c语言中的 while 结构
package main
​
import "fmt"
​
func main() {
        sum := 1
        for sum < 1000 {
                sum += sum
        }
        fmt.Println(sum)
}
  1. 无限循环
package main
​
​func main() {
        for {
​
        }
}

判断结构

  1. 条件可以不用在括号内:if conditon {}
  2. 条件判断前可以有前置的语句,语句中变量的作用域只在 if 内,包括 else

选择结构

  1. 每个 case 后面默认加了 break
  2. 每个 case 里面可以是语句
package main
​
import (
        "fmt"
        "runtime"
)
​
func main() {
        fmt.Print("Go runs on ")
        switch os := runtime.GOOS; os {
        case "darwin":
                fmt.Println("OS X.")
        case "linux":
                fmt.Println("Linux.")
        default:
                // freebsd, openbsd,
                // plan9, windows...
                fmt.Printf("%s.\n", os)
        }
}
  1. 从上往下执行,满足条件时停止
  2. Switch 可以可以不更条件语句,相当于 switch true
package main
​
import (
        "fmt"
        "time"
)
​
func main() {
        t := time.Now()
        switch {
        case t.Hour() < 12:
                fmt.Println("Good morning!")
        case t.Hour() < 17:
                fmt.Println("Good afternoon.")
        default:
                fmt.Println("Good evening.")
        }
}

defer

  1. 将函数的执行延迟到 return 之前
  2. 函数的参数是立刻执行的
  3. 多个 defer 是栈存储的,last in fast out

指针

  1. *T 代表 T 值的一个指针,指针的零值是 nil
  2. & 取指针,* 取指针的值,和C语言一样
  3. 不支持指针预算,和C语言不一样

结构体

  1. 结构体是 fields 的集合
package main
​
import "fmt"
​
// 声明
​
struct Point {
    X int
    Y int
}
​
func main() {
    p := Point{1, 2}
    fmt.Println(p) // 创建
    fmt.Println(p.X) // 取值
}
  1. 结构体可以通过指针引用
package main

import "fmt"
​
// 声明
struct Point {
    X int
    Y int
}
​
func main() {
    v := Point{1, 2}
​    p := &v
    fmt.Println(p.X) // 取值, 等价于 (*p).X
}

1. 结构体的字面量
package main
​
import "fmt"
​
type Vertex struct {
        X, Y int
}
​
var (
        v1 = Vertex{1, 2}  // has type Vertex
        v2 = Vertex{X: 1}  // Y:0 is implicit​
        v3 = Vertex{}      // X:0 and Y:0
        p  = &Vertex{1, 2} // has type *Vertex
)
​
func main() {
        fmt.Println(v1, p, v2, v3)
}
​
// {1 2} &{1 2} {1 0} {0 0}

数组

  1. 声明:[n]T,如 var a [10]int
  2. a [5]int := {1, 2, 3, 4, 5}
  3. 数组的长度是类型的一部分,不能重新指定长度

切片

  1. 声明:[]T

  2. A 是一个数组,那么,A[low:high] 构成一个切片,包括low,不包括high

  3. 切片不存储具体的数据,它只是描述底层数组的一部分,像是数组的一个引用

  4. 修改切片的数据,会修改相应的底层数组的数据,该数组的其他切片可以看到修改

  5. 数组和切片的区别
    func main() {
      x := [3]int{1, 2, 3} // 数组
      y := []int{1, 2, 3} // 像上面一样构建一个数组,然后创建一个切片引用它
    }

  6. 切片的 low 和 hight 可以省略,和 Python 的切片一样

  7. 切片有legth和capacity两个属性,length是切片中元素的个数;capacity是切片对应的底层数据的元组个数,从切片的第一个元素开始计数

  8. len(s) 和 cap(s)

  9. 切片的零值是 nil,nil 切片的 length 和 capacity 都是零,没有底层数组

  10. 用 make 函数创建切片,make 会创建一个零值的数组,返回一个指向数组的切片

package main
​​
func main() {
    a := make([]int, 5) // len(a) = 5
    b := make([]int, 0, 5) // len(b) = 0, cap(b) = 5
}
  1. 切片可以包含任意类型,包括其他的切片
package main
​
func main() {
    x := [][]int{
        []int{1, 2},
        {}int{3, 4}
    }
}
  1. 往切片添加元素:append([]T, vs ...T)
package main
​
​
func main() {
​
    x := []int{1, 2, 3}
    x = append(x, 4) 
    // 如果底层数组放不下添加的元素,那么会创建一个新的数组返回一个切片指向它
}
  1. 切片的遍历:range
package main
​
​
​
func main() {
    x := []int{1, 2, 3}
​
    for i, v := range x {
        // ...
    }
​
    // 省略 index
    for _, v := range x {
        // ...
​    }
​
    // 只要index
    for i := range x {​
        // ...
    }
}

字典

  1. 字典的零值是nil,不包含任何 key,也不能添加 key
  2. 用make创建一个指定类型的数组 var m ma[string]int
  3. 字典的字面值
package main

import "fmt"
​
type Vertex struct {
        Lat, Long float64
}
​
​
​
var m = map[string]Vertex{
        "Bell Labs": Vertex{​
                40.68433, -74.39967,
        },
        "Google": Vertex{
                37.42202, -122.08408,
        },
        // Vertex 可以省略​
}
​
​
​
func main() {
        fmt.Println(m)
}
  1. 操作字典
package main
​
func main() {
    var m = make(map[string]int)

    m['a'] = 1
    m['b'] = 2

    v := m['b']
    delete(m, 'b')

    // 'a' in m, ok is true
    if e1, ok := m['a'] {
        // ...
    }
​
    // 'b' not in m, ok is false, e2 是 0
    if e2, ok := m['b'] {
        // ...
    }
}

高阶函数

  1. 函数可以作为值传递给另外一个函数
package main

​
func compute(fn func(int, int) int) int {
​    return fn(1, 2)
}
​
​
func main() {
​
    fn := func(x int, y int) int {
        return x + y
    }
​
    compute(fn)
​
}
  1. 函数可以作为返回值,实现闭包
//A closure is a function value that references variables from outside its body.
//The function may access and assign to the referenced variables; 
//in this sense the function is "bound" to the variables.
package main
​
// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
        a, b := 0, 1​
        return func() int {
                r := a
                a, b = b, a + b
                return r
        }
}
​​
func main() {
        f := fibonacci()
        for i := 0; i < 10; i++ {​
                fmt.Println(f())
        }
}

方法

  1. Go 没有类,但可以在类型上定义方法
  2. 方法是有一个 receiver 参数的函数
package main
​
import (
    "fmt"
    "math"
)
​
struct Vertex {
    X, Y int
}
​
func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X * v.X + v.Y * v.Y)
}
​
// 谨记:方法只是一个有 receiver 参数的方法
func Abs(v Vertex) float64 {
    return math.Sqrt(v.X * v.X + v.Y * v.Y)
}
  1. 可以为非 struct 类型定义方法
package main
​
type MyFloat float64
​
func (f MyFloat) Abs() MyFloat {
        if f < 0 {
                return float64(-f)
        }
        return float64(f)
}
  1. 方法的 reveiver 可以使类型T的指针*T
  2. 指针可以修改值
  3. 值reveiver和函数参数一样,操作的值的拷贝
package main
​
import (
​        "fmt"
        "math"
)
​
type Vertex struct {
        X, Y float64
}
​
func (v Vertex) Abs() float64 {
        return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
​
func (v *Vertex) Scale(f float64) {
        v.X = v.X * f
        v.Y = v.Y * f
}
​
func main() {
        v := Vertex{3, 4}
        v.Scale(10) // ok,go 翻译成 (&v).Scale(10)
        p := &v
        p.Scale(100) //ok
        fmt.Println(v.Abs())
}
  1. 函数的指针参数必须传指针
  2. 方法的指针 receiver 传入的既可以是值,也可以使指针;方法的值receiver 传入的既可以是值,也可以使指针
  3. 使用指针 receiver 的两个原因:
  4. 修改 receiver 指向的值
  5. 避免每次调用方法都copy一份receiver,当 receiver 是一个很大的结构体时,值 receiver 会比较低效率

接口

  1. 接口类型是一组函数签名的集合
  2. 接口类型的值可以接收任意实现了接口方法的值
  3. 实现接口是隐式的,方法中并没有实现相关的字眼
  4. 接口值可以看出是一个包含接口值和一个具体类型的元组:(value, type),通过接口调一个方法执行的是具体类型上的同名方法
  5. 接口里面的具体值可以是nil,方法的可以在 receiver 为 nil 时调用
package main
​

import "fmt"
​
​
type I interface {
        M()
}
​
​
type T struct {
​
        S string
​
}
​

func (t *T) M() {
        if t == nil {
​
                fmt.Println("<nil>")
​
                return
​
        }
        fmt.Println(t.S)
}
​
​
​
func main() {
        var i I
​
        var t *T
        i = t
        describe(i)
        i.M()
​
​
        i = &T{"hello"}
        describe(i)
        i.M()
}
​
​
​
func describe(i I) {
        fmt.Printf("(%v, %T)\n", i, i)
}
​
​
​
/*
​
(<nil>, *main.T)
​
<nil>
​
(&{hello}, *main.T)
​
hello
​
*/
  1. 接口的具体类型不能是nil,如果类型是nil,再对一个nil接口调用方法,会触发运行时错误
package main

import "fmt"
​
type I interface {
        M()
}
​
func main() {​
        var i I
        describe(i)​
        i.M()
}
​​
func describe(i I) {
        fmt.Printf("(%v, %T)\n", i, i)
}

//panic: runtime error: invalid memory address or nil pointer dereference
  1. 没有定义方法的接口是一个空接口:interface{}
  2. 空接口可以接收任何类型
  3. 用来接收不知道具体类型的值,如函数参数可以接收多个类型的值(print)
  4. 类型断言(Type Assertions)
  5. 获取一个接口值的底层具体值:t := i.(T),断言接口值 i 的底层具体值类型是 T,如果不是T,触发一个 panic
  6. 测验一个接口值的类型是否是T:t, ok := i.(T),是则t是底层具体值,ok 为 true;否则t为类型T的零值,ok 为 false
  7. 多个类型断言的处理方式:
switch v := i.(type) {  // 注意这里是 type
case T:
    // here v has type T
case S:
    // here v has type S
default:
    // no match; here v has the same type as i
}
  1. 最常见的接口:
type Stringer interface {
    String() string
}
​
type Person struct {
        Name string
        Age  int
}
​
func (p Person) String() string {
        return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

异常

  1. Golang 使用 error 表示错误,error 类型是一个内建的接口类型
type error interface {
​
    Error() string
​
}
  1. 函数有时会返回 error 值,程序中需要对非 nil 的 error 做出处理,error 为 nil 表示成功
i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}
fmt.Println("Converted integer:", i)
  1. Reader 接口:填充给定的 byte 切片返回填充的数量和 error,当 stream 结束时返回 io.EOF error
    func (T) Read(b []byte) (n int, err error)

  2. TODO: 实现一个 reader

  3. TODO:rot13Reader

  4. Image 接口

Goroutines

  1. Goroutine 是在相同的地址空间跑的,访问共享内存是需要同步,可以使用 sync 模块的原语

  2. Channels 是有类型的管道,通过它可以接收和发送值
    ch := make(chan int) // 创建 channel

    ch <- 1 // 发送值

    v := <-ch // 接收值

  3. 管道的收发方必须处于 ready 状态,使用这个特性可以实现同步而不需要使用锁或者条件变量

package main
​​
import "fmt"
​
​
func sum(s []int, c chan int) {
        sum := 0
        for _, v := range s {
                sum += v
        }
        c <- sum // send sum to c
}
​
func main() {
        s := []int{7, 2, 8, -9, 4, 0}

        c := make(chan int)
        go sum(s[:len(s)/2], c)
        go sum(s[len(s)/2:], c)
        x, y := <-c, <-c // receive from c
​
        fmt.Println(x, y, x+y)
}
  1. Channel 可以缓存

  2. 只有缓存未满时可以往里面发送值

  3. Channel 为空值,取值方会阻塞

  4. Buffer over flow:fatal error: all goroutines are asleep - deadlock!
    ch := make(chan int, 100)

  5. 发送方可以对 ch 调用 close 方法表示没有更多的值发送了;接收方可以用 v, ok := <-ch 测试是否还有值,ok 为 false 是表示没有值了;for i := range ch 也可以;

  6. 只有发送方可以关闭channel,往关闭了的channel发送值会panic

  7. 管道的关闭不像文件,不是必须关闭的,只有当必须要告诉接收方没有值了才用

  8. Select 可以同时操作多个管道,select 会阻塞直到有一个 case 可以执行,如果有多个可以执行,select 会随机选择一个

package main
​
import "fmt"

​
func fibonacci(c, quit chan int) {
        x, y := 0, 1
        for {
                select {
                case c <- x:
                        x, y = y, x+y
                case <-quit:
                        fmt.Println("quit")
                        return
                }​
        }​
}
​
func main() {
        c := make(chan int)
        quit := make(chan int)
        go func() {
                for i := 0; i < 10; i++ {
                        fmt.Println(<-c)
                }
​
                quit <- 0
        }()
        fibonacci(c, quit)​
}
  1. Select 中的 default 会在没有其他 case 可执行时执行
select {
case i := <-c:
    // use i
default:
    // receiving from c would block​
}
  1. 使用 sync.Mutex 实现互斥锁,mu.Lock 加锁,mu.Unlock 解锁,可以 defer 确保解锁

How to Write Go Code

代码组织

  1. Go 代码是按包组织的
  2. 包是同一目录下的一组源文件集合,它们在一起编译
  3. 同一个包下的不同源文件中的函数、类型、变量和常量是相互可见的
  4. 一个仓库通常只有一个模块,位于仓库的根目录下,一个模块包含多个包,同级目录下会有一个go.mod描述模块的路径:模块中所有包的导入前缀
  5. 模块的路径不只是导入的前缀,也是模块的下载地址
  6. 导入路径就是模块的路径加上导入包的子路径,如:github.com/google/go-cmp/cmp
  7. 标准库的包没有模块的路径前缀
  8. Go 代码源文件第一行必须指定包

go mod

  1. 使用 go mod init + module_path 初始化模块,如:go mod init example.com/my/hello
  2. 使用 go install + module 安装模块,如:go install example.com/my/hello
  3. 模块的安装目录由
    GOPATH

and
GOBIN

这两个环境变量控制

  1. 使用 go env 查看 go 相关的环境变量,go env -w 设置变量,go env -u 取消设置
  2. 安装当前目录下的包可以简写为 go install . 或者 go install
  3. 使用 go env -w GOPROXY=https://goproxy.io 设置代理
  4. 使用 go mod tidy 安装依赖包,移除不再使用的包
  5. 移除所有下载的包:go clean -modcache

测试

  1. 使用 testing 模块
  2. 文件的命名:want_test.go
  3. 测试用例的命名:TestFunction(t *testing.T)
package morestrings
​
import "testing"
​
func TestReverseRunes(t *testing.T) {
        cases := []struct {
                in, want string
        }{
                {"Hello, world", "dlrow ,olleH"},
                {"Hello, 世界", "界世 ,olleH"},
                {"", ""},
        }
​
        for _, c := range cases {
                got := ReverseRunes(c.in)
                if got != c.want {
                        t.Errorf("ReverseRunes(%q) == %q, want %q", c.in, got, c.want)
                }
        }
}