变量定义
变量定义语法
- 使用var关键字,可放在函数内,也可放在包内
// var + 变量名 + 数据类型(有默认值)
var a int
var b string = "string"
// 通过赋值自动判断类型
var c = true
// 集中定义
var (
x = 1
y = 2
)
- 使用:=定义变量,只能在函数内使用
// := 用于赋初值
a, b := 1, 2
内建变量类型(builtin)
- bool, string
- (u)int, (u)int8, (u)int16, (u)int32, (u)int64, uintptr
- byte, rune
- float32, float64, complex64, complex128
强制类型转换
Golang要求强制类型转换,无隐式转换
a, b := 3, 4
var c int
c = int(math.Sqrt(float64(a * a + b * b)))
常量
使用const关键字定义常量,const数值可作为各种类型使用。
// 不确定类型
const a = 3
var b float64
b = a
// 合并定义
const (
c = 1
d = 2
)
枚举
const (
spring = 0
summer = 1
autumn = 2
winter = 3
)
// iota用在取值为0的const变量,后续变量依次加1
const (
spring = iota
summer
autumn
winter
)
变量定义要点
- 变量类型写在变量名之后
- 编译器可以推测变量类型
- Golang没有char,使用rune
- 原生支持复数类型
分支
if
const filename = "abc.txt"
contents, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(contents)
}
// 可以在if条件中赋值,赋值变量的作用域在if语句中
if contents, err := ioutil.ReadFile(filename); err != nil {
fmt.Println(err)
} else {
fmt.Println(contents)
}
switch
switch后可以没有表达式,case结束后自动break,通过fallthrough不使用自动break。
func grade(score int) {
grade := ""
switch {
case score < 0:
panic(fmt.Sprintf("Wrong score: %d\n", score))
case score < 60:
grade = "C"
case score < 80:
grade = "B"
case score < 90:
grade = "A"
case score < 100:
// 自动break,除非出现fallthrough
fallthrough
case score >= 100:
grade = "S"
}
fmt.Println(grade)
}
循环
for条件没有括号,没有while。
func convertToBin(n int) string {
result := ""
for ; n > 0; n /= 2 {
lsb := n % 2
result = strconv.Itoa(lsb) + result
}
return result
}
func printFile(filename string) {
file, err := os.Open(filename)
if err != nil {
panic(err)
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}
func forever() {
// while true
for {
fmt.Println("forever")
}
}
函数
- 函数声明的顺序为:关键字,函数名,参数,返回类型,函数体。
- 函数允许有多个返回值,并能在函数体内拿到返回值。
- 函数可以作为另一个函数的参数
- 函数支持可变参数列表
func div(a, b int) (q, r int) {
// q = a / b
// r = a % b
// return
return a / b, a % b
}
// 使用_抛弃返回值
a, _ := div(12, 7)
// 使用另一个函数作为参数
func apply(operation func(a, b int), a, b int) {
operation(a, b)
}
// 使用匿名函数
apply(func(a, b int) {
fmt.Println(a, b)
}, 1, 2)
// 可变参数列表
func sum(numbers ...int) int {
s := 0
for i := range numbers {
s += numbers[i]
}
return s
}
指针
Golang只有值传递一种方式。
// 使用指针操作
func change(pa *int) {
*pa++
}
a := 5
change(&a)
func swap(x, y *int) {
*x, *y = *y, *x
}
数组
数组长度写在类型之前,[10]int和[20]int是不同的类型。数组是值复制。
var arr1 [5]int
arr2 := [3]int{1, 3, 5}
arr3 := [...]int{2, 4, 6}
var arr4 [4][5]int
遍历数组
for i := 0; i < len(arr3); i++ {
fmt.Println(arr3[i])
}
for i := range arr3 {
fmt.Println(arr3[i])
}
// 按下标和值遍历
for i, v := range arr3 {
fmt.Println(i, v)
}
切片
数组切片相当于数据的一个视图,可以通过切片改变数组的值。
func updateSlice(s []int) {
s[0] = 100
}
func main() {
array := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := array[2:6]
s2 := array[2:]
s3 := array[:6]
s4 := array[:]
fmt.Println(s1, s2, s3, s4) // [2 3 4 5] [2 3 4 5 6 7] [0 1 2 3 4 5] [0 1 2 3 4 5 6 7]
updateSlice(s2)
fmt.Println(s2) // [100 3 4 5 6 7]
updateSlice(s3)
fmt.Println(s3) // [100 1 100 3 4 5]
// 切片扩展
s5 := s3[6:8]
fmt.Println(s5)
// 添加元素
s6 := append(s5, 8)
fmt.Println(s6, array)
fmt.Println(array[:8])
// 通过make创建切片
// 长度为10的切片
x := make([]int, 10)
// 长度为10,容量为16的切片
y := make([]int, 10, 16)
// 拷贝数组元素
copy(x, y)
// 截取数组元素
z := append(x[:2], x[3:]...)
}
切片底层维护了一个数组,ptr指向切片的首个元素,len表示切片的长度,cap表示底层数组的从ptr到最末的元素数量。切片可以向后扩展,不可向前扩展。切片添加元素如果会超过底层数组的cap,Golang会分配更大的底层数组(原容量*2进行扩展),由于值传递的关系,必须接收append方法的返回值。
map
通过map[K]v的形式定义map。除了slice、map、function的内建类型和struct类型都可以作为map的key。
m := map[string]string{
"name": "wch",
"age": "23",
}
m2 := make(map[string]int)
// 无序遍历map
for k, v := range m {
fmt.Println(k, v)
}
// map读值
name := m["name"]
// 判断key是否存在
grade, exist := m["grade"]
// 删除
delete(m, "name")
rune
Golang的rune相当于java的char,中文字符在Golang中占3个字节,使用 utf8.RuneCountInString
获取字符数,用 len
获取字节长度,使用 []byte
获取字节。
s := "学习Golang!"
fmt.Println(s)
// ch是rune类型,中文字符占3个字节
for i, ch := range s {
fmt.Printf("(%d %X) ", i, ch)
}
fmt.Println()
fmt.Println("rune count in string:", utf8.RuneCountInString(s))
bytes := []byte(s)
for len(bytes) > 0 {
ch, size := utf8.DecodeRune(bytes)
bytes = bytes[size:]
fmt.Printf("size= %d, rune=%c\n", size, ch)
}
for i, ch := range []rune(s) {
fmt.Printf("(%d %c)", i, ch)
}
fmt.Println()
面向对象
Golang仅支持封装,不支持继承和多态,没有class,只有struct,通过struct来定义对象。
type treeNode struct {
value int
left, right *treeNode
}
func createNode(value int) *treeNode {
return &treeNode{value: value}
}
func main() {
// 通过多种方式声明对象
var root treeNode
root = treeNode{value: 0}
root.left = &treeNode{}
root.right = &treeNode{1, nil, nil}
root.right.left = new(treeNode)
root.right.right = createNode(3)
}
Golang支持显式定义方法接收者,值/指针接收者均可接收值/指针。当需要改变内容或结构过大时需要使用指针接收者,值接收者是Golang特有的
// 指定方法接收者
func (node treeNode) print() {
fmt.Println(node.value)
}
func (node *treeNode) setValue(value int) {
node.value = value
}
func main() {
node := treeNode{}
node.setValue(100)
node.print()
pNode := &node
pNode.setValue(101)
pNode.print()
}
封装
Golang的可见性(针对包)使用首字母来控制,首字母大写表示public,首字母小写表示private。每个目录都是一个包,main包包含可执行入口。为结构定义的方法必须放在同一个包内,可以是不同的文件。Golang提供了两种扩展系统类型和已封装类型的方法:
- 定义别名:通过type为已有类型定义一个别名,针对新的结构体进行扩展。
type Queue []int
func (q *Queue) Push(v int) {
*q = append(*q, v)
}
func (q *Queue) Pop() int {
head := (*q)[0]
*q = (*q)[1:]
return head
}
func (q *Queue) IsEmpty() bool {
return len(*q) == 0
}
- 使用组合:定义新的结构体,其中一个成员是需要扩展的类型的指针。
type EnhanceNode struct {
node *Node
}
func (enhanceNode *EnhanceNode) PostOrder() {
if nil == enhanceNode || nil == enhanceNode.node {
return
}
left := EnhanceNode{enhanceNode.node.Left}
right := EnhanceNode{enhanceNode.node.Right}
left.PostOrder()
right.PostOrder()
enhanceNode.PostOrder()
}
GOROOT、GOPATH、go get
GOROOT一般为下载的sdk路径,GOPATH为用户可定义的路径,用户源码和通过go get命令下载的第三方库在GOPATH的src目录下。
gopm是go get的镜像工具,通过 go get -g -v github.com/gpmgo/gopm
下载gopm。
// 下载
gopm get -g -v Golang.org/x/tools/cmd/goimports
// 更新
gopm get -g -v -u Golang.org/x/tools/cmd/goimports
// 安装到GOPATH的bin目录
go install Golang.org/x/tools/cmd/goimports
接口
接口的实现是隐式的,只要实现接口的方法。
duck typing
“像鸭子走路,像鸭子叫(长得像鸭子),那么就是鸭子”,意为描述事物的外部行为而非内部结构。严格说go属于结构化类型系统,类似duck typing。
接口变量里有什么
- 接口自带指针
- 接口变量同样采用值传递,几乎不需要使用接口的指针
- 指针接收者只能以指针的方式使用,值接收者都可以
- interface{}可以代表任何类型
- 定义接口
type Retriever interface {
Get(url string) string
}
func download(r Retriever) string {
return r.Get("http://www.baidu.com")
}
- 实现接口
type Retriever struct {
UserAgent string
Timeout time.Duration
}
func (r *Retriever) Get(url string) string {
resp, err := http.Get(url)
if nil != err {
panic(err)
}
result, err := httputil.DumpResponse(resp, true)
resp.Body.Close()
if nil != err {
panic(err)
}
return string(result)
}
- 调用接口
var r Retriever
r = &real.Retriever{
UserAgent: "Mozilla/5.0",
Timeout: time.Minute,
}
fmt.Println(download(r))
// Type Assertion
if retriever, ok := r.(*real.Retriever); ok {
fmt.Printf("%T %v\n", retriever, retriever)
}
- 接口组合
type HttpExecute interface {
Retriever
Poster
}
函数式编程
- 函数是一等公民:参数,变量,返回值都可以是函数。
- 高阶函数:函数的参数可以是函数
- 函数->闭包
func adder() func(int) int {
sum := 0
return func(i int) int {
sum += i
return sum
}
}
func main() {
// f中不仅是一个函数,还有对变量sum的引用
f := adder()
for i := 0; i < 10; i++ {
fmt.Println(f(i))
}
}
错误处理和资源管理
defer
用于指定在函数结束之前执行,可以用于关闭文件、释放锁等。
func tryDefer() {
// 在函数借结束之前执行
defer fmt.Println("execute last...")
defer fmt.Println("execute before last...")
fmt.Println("execute")
}
panic
- 停止当前函数执行
- 一直向上返回,执行每一层的defer
- 如果没有遇见recover,程序退出
recover
- 仅在defer调用中使用
- 获取panic的值
- 如果无法处理,重新panic
func tryRecover() {
defer func() {
r := recover()
if err, ok := r.(error); ok {
fmt.Println("error:", err)
} else {
panic(fmt.Sprintf("unknown error: %v", r))
}
}()
panic(errors.New("this is an error"))
}
测试
测试格式
- 测试文件与待测试代码放在同一目录下
- 测试文件名以
_test.go
结尾 - 测试函数名为
TestXxx
或Test_xxx
的格式,使用其它类型测试,如性能测试则函数名为BenchmarkXxx
或 ```Benchmark_xxx`` 的格式
传统测试
- 传统测试数据与逻辑混在一起
- 传统测试的出错信息不明确
- 传统测试一旦出错测试即全部结束
表格驱动测试
- 测试代码
func calcTriangle(a, b int) int {
return int(math.Sqrt(float64(a*a + b*b)))
}
- 表格驱动测试
分离了测试数据和测试逻辑,明确了出错信息,可以部分失败。
func TestCalcTriangle(t *testing.T) {
tests := []struct{ a, b, c int }{
{3, 4, 5},
{5, 12, 13},
{8, 15, 18},
}
for _, tt := range tests {
if actual := calcTriangle(tt.a, tt.b); actual != tt.c {
t.Errorf("calcTriangle(%d %d); got %d; expected %d", tt.a, tt.b, actual, tt.c)
}
}
}
性能测试
执行b.N次,由Golang决定具体次数,测试完成后在控制台打印执行次数和平均执行时间。
func BenchmarkTriangle(b *testing.B) {
x, y, z := 8, 15, 17
for i := 0; i < b.N; i++ {
actual := calcTriangle(x, y)
if actual != z {
b.Errorf("calcTriangle(%d %d); got %d; expected %d", x, y, actual, z)
}
}
}
测试命令行工具
# 测试命令
go test
# 代码覆盖率测试
go test -cover
# 生成代码测试覆盖率文件
go test -coverprofile c.out
# 代码覆盖率测试html
go tool cover -html=c.out
# 性能测试
go test -bench .
# 生成性能报告
go test -bench . -cpuprofile cpu.out
文档
Golang提供 go doc
命令查看由注释组成的文档,通过 godoc -http :6060
命令在本地6060端口生成html文档。在test文件中建立名为 ExampleXxx_xxx
的函数,通过特定格式生成代码实例文档。
func ExampleQueue_Pop() {
queue := Queue{}
queue.Push(1)
fmt.Println(queue.Pop())
// Output:
// 1
}
go routine
协程Coroutine
- 轻量级“线程”
- 非抢占式多任务处理,由协程主动交出控制权
- 编译器/解释器/虚拟机层面的多任务
- 多个协程可能在一个或多个线程上运行
goroutine定义
- 调用任何函数前只需加上
go
关键字就可以交给调度器执行 - 不需要在定义时区分是否为异步函数
- 调度器会在合适的点进行切换(I/O、select、channel、等待锁、函数调用等),或手动调用
runtime.Gosched()
进行切换 - 使用race来检测数据访问冲突,
go run -race xxx.go
channel
相对于通过共享内存来通信,channel通过通信来共享内存。
- chanel可以作为参数传递
- 可以设置channel的buffer
- 发送方负责关闭channel,接收方通过可以通过
range
判断channel是否传递完毕
使用select进行调度
select可以用来监听IO操作,可以同时监听多个channel的消息状态。