Golang 1:基础
本文总结了 Golang 的基本使用。
Hello World
package main
import "fmt"
func main() {
fmt.Println("Hello, World")
}
语法基础
包
- Go 程序由 package 组成
- 入口在 main package
- 在程序中引入包的方式:
a. "fmt"
b. "math/rand" - 按照惯例包的名称是包路径的最后一个元素,如 "manth/rand" 的名称为 "rand"
- Import
import "fmt"
import "math"
等价于
import (
"fmt"
"math"
)
- 在一个 package 中,以大写字母开头的属性或者函数是公开的;已小写字母开头的属性或者非法是私有的。
函数
-
定义
func add(x int, y int) int {
return x + y
} -
相邻参数的类型如果是一样的,可以只写一次类型
func add(x, y int) int {
return x + y
} -
函数可以有多个返回值
func swap(x, y int) (int, int) {
return y, x
} -
可以命名返回值,命名的参数会被当做定义在函数顶部的变量,提示返回值的含义,没有参数的 return 语句返回命名返回值变量,称为裸 return,只应该在短的函数使用,在长函数中不利于阅读。
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
} -
命名参数会被初始化为类型的零值
变量
- Var 声明 package 和 function 级别的变量:var x, y int
- Var 声明变量时可以指定初始值:var x, y int = 1, 2
- Var 声明变量时指定初始值可以省略类型:var x, y, z = 1, true, "jack"
- 函数内部可以用 := 声明变量,外部不行
- 变量类型
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
- 变量的零值:
- 数值型:0
- 布尔型:false
- 字符串型:""
- 类型转换:T(v)
- 类型推导:变量没有声明类型时,由赋予值时右边值的类型决定
常量
- 常量用 const 声明,不能用 :=
- 多个常量可以用 cont () 一次声明
循环结构
- 基本结构
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
- 初始和后置条件可以为空
package main
import "fmt"
func main() {
sum := 1
for ; sum < 1000; {
sum += sum
}
fmt.Println(sum)
}
- 可以省略 ;,相当于c语言中的 while 结构
package main
import "fmt"
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}
- 无限循环
package main
func main() {
for {
}
}
判断结构
- 条件可以不用在括号内:if conditon {}
- 条件判断前可以有前置的语句,语句中变量的作用域只在 if 内,包括 else
选择结构
- 每个 case 后面默认加了 break
- 每个 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)
}
}
- 从上往下执行,满足条件时停止
- 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
- 将函数的执行延迟到 return 之前
- 函数的参数是立刻执行的
- 多个 defer 是栈存储的,last in fast out
指针
- *T 代表 T 值的一个指针,指针的零值是 nil
- & 取指针,* 取指针的值,和C语言一样
- 不支持指针预算,和C语言不一样
结构体
- 结构体是 fields 的集合
package main
import "fmt"
// 声明
struct Point {
X int
Y int
}
func main() {
p := Point{1, 2}
fmt.Println(p) // 创建
fmt.Println(p.X) // 取值
}
- 结构体可以通过指针引用
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}
数组
- 声明:[n]T,如 var a [10]int
- a [5]int := {1, 2, 3, 4, 5}
- 数组的长度是类型的一部分,不能重新指定长度
切片
-
声明:[]T
-
A 是一个数组,那么,A[low:high] 构成一个切片,包括low,不包括high
-
切片不存储具体的数据,它只是描述底层数组的一部分,像是数组的一个引用
-
修改切片的数据,会修改相应的底层数组的数据,该数组的其他切片可以看到修改
-
数组和切片的区别
func main() {
x := [3]int{1, 2, 3} // 数组
y := []int{1, 2, 3} // 像上面一样构建一个数组,然后创建一个切片引用它
} -
切片的 low 和 hight 可以省略,和 Python 的切片一样
-
切片有legth和capacity两个属性,length是切片中元素的个数;capacity是切片对应的底层数据的元组个数,从切片的第一个元素开始计数
-
len(s) 和 cap(s)
-
切片的零值是 nil,nil 切片的 length 和 capacity 都是零,没有底层数组
-
用 make 函数创建切片,make 会创建一个零值的数组,返回一个指向数组的切片
package main
func main() {
a := make([]int, 5) // len(a) = 5
b := make([]int, 0, 5) // len(b) = 0, cap(b) = 5
}
- 切片可以包含任意类型,包括其他的切片
package main
func main() {
x := [][]int{
[]int{1, 2},
{}int{3, 4}
}
}
- 往切片添加元素:append([]T, vs ...T)
package main
func main() {
x := []int{1, 2, 3}
x = append(x, 4)
// 如果底层数组放不下添加的元素,那么会创建一个新的数组返回一个切片指向它
}
- 切片的遍历: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 {
// ...
}
}
字典
- 字典的零值是nil,不包含任何 key,也不能添加 key
- 用make创建一个指定类型的数组 var m ma[string]int
- 字典的字面值
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)
}
- 操作字典
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'] {
// ...
}
}
高阶函数
- 函数可以作为值传递给另外一个函数
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)
}
- 函数可以作为返回值,实现闭包
//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())
}
}
方法
- Go 没有类,但可以在类型上定义方法
- 方法是有一个 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)
}
- 可以为非 struct 类型定义方法
package main
type MyFloat float64
func (f MyFloat) Abs() MyFloat {
if f < 0 {
return float64(-f)
}
return float64(f)
}
- 方法的 reveiver 可以使类型T的指针*T
- 指针可以修改值
- 值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())
}
- 函数的指针参数必须传指针
- 方法的指针 receiver 传入的既可以是值,也可以使指针;方法的值receiver 传入的既可以是值,也可以使指针
- 使用指针 receiver 的两个原因:
- 修改 receiver 指向的值
- 避免每次调用方法都copy一份receiver,当 receiver 是一个很大的结构体时,值 receiver 会比较低效率
接口
- 接口类型是一组函数签名的集合
- 接口类型的值可以接收任意实现了接口方法的值
- 实现接口是隐式的,方法中并没有实现相关的字眼
- 接口值可以看出是一个包含接口值和一个具体类型的元组:(value, type),通过接口调一个方法执行的是具体类型上的同名方法
- 接口里面的具体值可以是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
*/
- 接口的具体类型不能是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
- 没有定义方法的接口是一个空接口:interface{}
- 空接口可以接收任何类型
- 用来接收不知道具体类型的值,如函数参数可以接收多个类型的值(print)
- 类型断言(Type Assertions)
- 获取一个接口值的底层具体值:t := i.(T),断言接口值 i 的底层具体值类型是 T,如果不是T,触发一个 panic
- 测验一个接口值的类型是否是T:t, ok := i.(T),是则t是底层具体值,ok 为 true;否则t为类型T的零值,ok 为 false
- 多个类型断言的处理方式:
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
}
- 最常见的接口:
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)
}
异常
- Golang 使用 error 表示错误,error 类型是一个内建的接口类型
type error interface {
Error() string
}
- 函数有时会返回 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)
-
Reader 接口:填充给定的 byte 切片返回填充的数量和 error,当 stream 结束时返回 io.EOF error
func (T) Read(b []byte) (n int, err error) -
TODO: 实现一个 reader
-
TODO:rot13Reader
-
Image 接口
Goroutines
-
Goroutine 是在相同的地址空间跑的,访问共享内存是需要同步,可以使用 sync 模块的原语
-
Channels 是有类型的管道,通过它可以接收和发送值
ch := make(chan int) // 创建 channel
ch <- 1 // 发送值
v := <-ch // 接收值 -
管道的收发方必须处于 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)
}
-
Channel 可以缓存
-
只有缓存未满时可以往里面发送值
-
Channel 为空值,取值方会阻塞
-
Buffer over flow:fatal error: all goroutines are asleep - deadlock!
ch := make(chan int, 100) -
发送方可以对 ch 调用 close 方法表示没有更多的值发送了;接收方可以用 v, ok := <-ch 测试是否还有值,ok 为 false 是表示没有值了;for i := range ch 也可以;
-
只有发送方可以关闭channel,往关闭了的channel发送值会panic
-
管道的关闭不像文件,不是必须关闭的,只有当必须要告诉接收方没有值了才用
-
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)
}
- Select 中的 default 会在没有其他 case 可执行时执行
select {
case i := <-c:
// use i
default:
// receiving from c would block
}
- 使用 sync.Mutex 实现互斥锁,mu.Lock 加锁,mu.Unlock 解锁,可以 defer 确保解锁
How to Write Go Code
代码组织
- Go 代码是按包组织的
- 包是同一目录下的一组源文件集合,它们在一起编译
- 同一个包下的不同源文件中的函数、类型、变量和常量是相互可见的
- 一个仓库通常只有一个模块,位于仓库的根目录下,一个模块包含多个包,同级目录下会有一个go.mod描述模块的路径:模块中所有包的导入前缀
- 模块的路径不只是导入的前缀,也是模块的下载地址
- 导入路径就是模块的路径加上导入包的子路径,如:github.com/google/go-cmp/cmp
- 标准库的包没有模块的路径前缀
- Go 代码源文件第一行必须指定包
go mod
- 使用 go mod init + module_path 初始化模块,如:go mod init example.com/my/hello
- 使用 go install + module 安装模块,如:go install example.com/my/hello
- 模块的安装目录由
GOPATH
and
GOBIN
这两个环境变量控制
- 使用 go env 查看 go 相关的环境变量,go env -w 设置变量,go env -u 取消设置
- 安装当前目录下的包可以简写为 go install . 或者 go install
- 使用 go env -w GOPROXY=https://goproxy.io 设置代理
- 使用 go mod tidy 安装依赖包,移除不再使用的包
- 移除所有下载的包:go clean -modcache
测试
- 使用 testing 模块
- 文件的命名:want_test.go
- 测试用例的命名: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)
}
}
}