Go web编程学习(一)


title: Go web编程学习(一)
date: 2019-05-01 14:02:17
tags:
- study note
- Go web
categories:
- Study
- Go


关于Go语言Web编程的一些学习笔记,这是第一篇,主要是粗略过一遍Go语言语法,后面会写Go web编程的一些东西。


前言

不知道什么时候开始,整个人变得浮躁起来,沉不下心来学东西。其实早就想静下心来学点东西了,比如说Go语言、Java spring等,所以这次趁着五一小长假,来学习一波。


环境

这次学习我主要是用一本书,名字叫Go Web编程,在Gitbook上,是一本开源书籍。Go Web编程
编译器我用的是: Visual Studio Code 2017
Go的版本是: go1.11.5 windows/amd64


话不多说,开始学吧。

Go关键字

Go是一门类似C的编译型语言,但是它的编译速度非常快。这门语言的关键字总共二十五个,把这二十五个关键字用会,Go语言差不多就入门了吧。
二十五个关键字如下:

break       default         func        interface       select
case        defer           go          map             struct
chan        else            goto        package         switch
const       fallthrough     if          range           type
continue    for             import      return          var

Go包机制理解

先从hello world讲起。先创建一个hello.go

package main

import "fmt"

func main() {
    fmt.Printf("Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい\n")
}

这段程序输出如下:
Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい

现在来分析一下这段代码

看到第一行package main


image

归纳一下就是package 遵从以下规则。

  1. package是最基本的分发单位和工程管理中依赖关系的体现
  2. 每个Go语言源代码文件开头都必须要有一个package声明,表示源代码文件所属包
  3. 要生成Go语言可执行程序,必须要有名为main的package包,且在该包下必须有且只有一个main函数
  4. 同一个路径下只能存在一个package,一个package可以由多个源代码文件组成

这个package机制还是要搞懂一下的,不然程序运行起来莫名出错。这里我也不知道该怎么表达清楚(水平有限233),只能放两个例子帮助大家理解。
第一个例子: 生成使用自己的package
GOPATH\src目录下,新建test文件夹,test文件夹里新建test.go

package test

import "fmt"

func Test() { 
    fmt.Printf("this is package test test.go\n")
}

注意,这里第一行是package test,说明这是一个包文件,它有一个函数命名是Test,首字母大写了,这里说明一下,在Go语言里,有一个简单的规则:如果一个名字是大写字母开头的,那么该名字是导出的(汉字不区分大小写,因此汉字开头的名字是没有导出的)

这是一个简单的包文件,命令行在test文件夹下输入 go install即可编译生成test.a包文件。

image

然后就能在别的go程序里使用它了。如在GOPATH\src\hello文件夹下main.go文件中导入调用该包及其Test()函数。
image

第二个例子: 同一目录下的多文件package
GOPATH\src\hello文件夹下,创建两个文件,分别是main.go、hello.go代码分别如下
main.go

package main

import (
    "fmt"
    "test"
)

func test1() {
    fmt.Printf("Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい\n")
}

func main() {
    test.Test()
    test1()
    hello()
}

hello.go

package main

import "fmt"

func hello() {
    fmt.Printf("hello, this is hello.go\n")
}

这里main.go程序下调用了hello.go里的函数,这里的hello.go类似于C中的.h文件,我们可以命令行在hello文件夹内输入go build .,以此生成hello.exe,然后执行。可以看到,生成的hello.exe执行能调用hello.go里的函数,这是同一路径多文件package的情况。

image


Go基础

变量类型

这里讲讲定义变量、常量、Go内置类型以及Go程序设计中的一些技巧。
Go语言的数据类型有
整型:
uint8、uint16、uint32、uint64、int8、int16、int32、int64

浮点数:
float32、float64

复数:
complex64、complex128分别对应float32、float64两种浮点数精度
内置的complex函数用来构建复数,内置的real、imag函数分别返回复数的实部和虚部

var x complex128 = complex(1,2) //1+2i
var y complex128 = complex(3,4) //3+4i
fmt.Println(real(x*y)) //-5
fmt.Println(imag(x*y))  //10

布尔型:
只能是true、false

字符串:
字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。
用双引号""或反引号``定义,反引号可用于定义多行字符串类型是string。
具有len方法、切片、+号连接等操作
使用var声明方法:var s string = "miracle778"

字符串是不可修改的,如果非要修改的话,可以先转为byte数组,然后再更改需要改的单位字符。如下图


image

错误类型:error
Go内置有一个error类型,专门用来处理错误信息,Go的package里面还专门有一个包errors来处理错误

其他类型如iota枚举类型,就不讲了,用的时候网上找找就好了。

变量定义方式

一般声明,使用var <变量名> <变量类型>进行声明
如:

var str string = "字符串" //字符串变量
var num uint32 = 77777  //uint32类型变量

简短声明,使用<变量名>:=<表达式>来声明初始化变量。它会根据表达式自动推导变量的类型。
因为简短灵活的特点,简短变量声明被广泛用于大部分的局部变量声明和初始化。而var形式的声明往往适用于需要显示指明变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方

变量定义方式可以一次定义多个变量,like python
var a,b,c int = 1,2,3
a,b,c := 1,2,3

变量要注意的点

  1. 下划线_是个特殊的变量名,任何赋予它的值都会被丢弃
    如:_,a = 7,8 这里将8赋给a,并同时丢弃7

  2. Go对于已声明但未使用的变量会在编译阶段报错

Go语言的一些默认规则

  1. 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公用变量;小写字母开头的就是不可导出的,是私有变量。
  2. 大写字母开头的函数也是一样,相当于class中的带public关键词的公有函数;小写字母开头的就是有private关键词的私有函数。

Array 数组

数组定义方式

  1. var声明 var [length]如:var arr [7]int
    通过var声明后,数组各项元素默认为0或空

    image

    通过var声明并初始化
    var arr = [4]string{"a","b"}

  2. 简短声明

    a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组
    
    b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0
    
    c := [...]int{4, 5, 6} // 可以省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度
    

    数组初始化的时候可以使用索引:值的形式赋值,如下图

    image

  3. 二维数组声明

a:=[2][3]int{{1,2},{3,4,5}}

数组可以像python一样切片

关于数组的一些容易混淆的点

由于长度也是数组类型的一部分,因此[3]int与[4]int是不同的类型,数组也就不能改变长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。

slice 动态数组

slice并不是真正意义上的动态数组,而是一个引用类型。slice总是指向一个底层array,slice的声明也可以像array一样,只是不需要长度。
slice var声明举例 var a = []byte{'a','b','c'},简短声明 a := []int

slice可以从一个数组或一个已经存在的slice中再次声明。

// 声明一个含有10个元素元素类型为byte的数组
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}

// 声明两个含有byte的slice
var a, b []byte

// a指向数组的第3个元素开始,并到第五个元素结束,
a = ar[2:5]
//现在a含有的元素: ar[2]、ar[3]和ar[4]

// b是数组ar的另一个slice
b = ar[3:5]
// b的元素是:ar[3]和ar[4]

slice的一些切片操作,跟python类似

// 声明一个数组
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 声明两个slice
var aSlice, bSlice []byte

// 演示一些简便操作
aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c
aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j
aSlice = array[:]  // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素

// 从slice中获取slice
aSlice = array[3:7]  // aSlice包含元素: d,e,f,g,len=4,cap=7
bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f
bSlice = aSlice[:3]  // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f
bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h
bSlice = aSlice[:]   // bSlice包含所有aSlice的元素: d,e,f,g

slice是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的aSlice和bSlice,如果修改了aSlice中元素的值,那么bSlice相对应的值也会改变。

对于slice有几个有用的内置函数:

  1. len 获取slice的长度
  2. cap 获取slice的最大容量
  3. append 向slice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice
  4. copy 函数copy从源slice的src中复制元素到目标dst,并且返回复制的元素的个数

对于上面4个内置函数,len函数自然不用多说,返回slice长度。长度和最大容量之间关系如下图示


image

下面演示下另外三个函数的用法
其中特别注意append函数

append函数会改变slice所引用的数组的内容,从而影响到引用同一数组的其它slice。 但当slice中没有剩余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的slice数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的slice则不受影响。

上面这段话意思如下图示,当b slice使用append添加元素后,如果添加后的长度小于cap最大长度时,原数组中对应位置元素也发生变化。如下面a数组,append函数执行后发生改变。


image

上面那段话还提到当使用append函数slice没有剩余空间时,此时动态分配新的数组空间,返回的slice数组指针将指向这个空间,原数组内容不变,其他引用原数组的slice不受影响


image

copy函数主要是切片(slice)的拷贝,不支持数组。将第二个slice里的元素拷贝到第一个slice里,拷贝的长度为两个slice中长度较小的长度值


image

此外还需介绍下append函数的三种用法。

  1. slice := append([]int{1,2,3},4,5,6)
    fmt.Println(slice) //[1 2 3 4 5 6]
    
  2. slice := append([]int{1,2,3},[]int{4,5,6}...)    //末尾记住加三个点
    fmt.Println(slice) //[1 2 3 4 5 6]
    
  3. 特殊用法,将字符串当作[]byte类型作为第二个参数传入
    bytes := append([]byte("hello"),"world"...)
    

append函数返回值必须要有变量接受,不然会报错

map

map也就是Python中字典的概念,它的格式为map[keyType]valueType

声明方式

  1. 用var声明,使用前需要用make初始化


    image
  2. 简单声明


    image

初始化

  1. var dict = map[int]string{0:"miracle"}
  2. dict := map[int]string{0:"Miracle778"}

使用map过程中注意下面几点

  1. map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取
  2. map的长度是不固定的,也就是和slice一样,也是一种引用类型
  3. 内置的len函数同样适用于map,返回map拥有的key的数量
  4. map的值可以很方便的修改,通过numbers["one"]=11可以很容易的把key为one的字典值改为11
  5. map和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制

map的初始化可以通过key:val的方式初始化值,同时map内置有判断是否存在key的方式

// 初始化一个字典
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
// map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true
csharpRating, ok := rating["C#"]
if ok {
    fmt.Println("C# is in the map and its rating is ", csharpRating)
} else {
    fmt.Println("We have no rating associated with C# in the map")
}

delete(rating, "C")  // 删除key为C的元素

上面说过了,map也是一种引用类型,如果两个map同时指向一个底层,那么一个改变,另一个也相应的改变:

m := make(map[string]string)
m["Hello"] = "Bonjour"
m1 := m
m1["Hello"] = "Salut"  // 现在m["hello"]的值已经是Salut了

make、new操作

make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配。
区别是new返回一个指针,make返回引用

内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。

内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始化了内部的数据结构,填充适当的值。

make两个用法

  1. map
dict := make(map[int]string)
dict[0],dict[1] = "name","passwd"
  1. slice
slice := make([]int,2,6)
//返回长度为2 容量为6的slice

下图说明了make、new区别


image

零值

罗列部分类型的零值

int     0
int8    0
int32   0
int64   0
uint    0x0
rune    0 //rune的实际类型是 int32
byte    0x0 // byte的实际类型是 uint8
float32 0 //长度为 4 byte
float64 0 //长度为 8 byte
bool    false
string  ""

Go流程和函数

if语句

Go的if语句不需要括号,Go的if还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下面代码

// 计算获取值x,然后根据x返回的大小,判断是否大于10。
if x := computedValue(); x > 10 {
    fmt.Println("x is greater than 10")
} else {    //else的位置要在 {同一行
    fmt.Println("x is less than 10")
}

//这个地方如果这样调用就编译出错了,因为x是条件里面的变量
fmt.Println(x)

多条件if

if integer == 3 {
    fmt.Println("The integer is equal to 3")
} else if integer < 3 {
    fmt.Println("The integer is less than 3")
} else {
    fmt.Println("The integer is greater than 3")
}

goto语句

用法,注:标签名是大小写敏感的

func myFunc() {
    i := 0
Here:   //这行的第一个词,以冒号结束作为标签
    println(i)
    i++
    goto Here   //跳转到Here去
}

for语句

Go语言的for语句既可以用来循环读取数据,又可以当作while来控制逻辑,还能迭代操作。

  1. 循环读数据
package main
import "fmt"

func main(){
    sum := 0;
    for index:=0; index < 10 ; index++ {
        sum += index
    }
    fmt.Println("sum is equal to ", sum)
}
// 输出:sum is equal to 45
  1. 当while用
sum := 1
for sum < 1000 {
    sum += sum
}
  1. 配合range 迭代
for k,v:=range map {
    fmt.Println("map's key:",k)
    fmt.Println("map's val:",v)
}

这里因为Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用_来丢弃不需要的返回值

for _, v := range map{
    fmt.Println("map's val:", v)
}

switch语句

Go的switch非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;而如果switch没有表达式,它会匹配true

i := 10
switch i {
case 1:
    fmt.Println("i is equal to 1")
case 2, 3, 4:
    fmt.Println("i is equal to 2, 3 or 4")
case 10:
    fmt.Println("i is equal to 10")
default:
    fmt.Println("All I know is that i is an integer")
}

Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码。

integer := 6
switch integer {
case 4:
    fmt.Println("The integer was <= 4")
    fallthrough
case 5:
    fmt.Println("The integer was <= 5")
    fallthrough
case 6:
    fmt.Println("The integer was <= 6")
    fallthrough
case 7:
    fmt.Println("The integer was <= 7")
    fallthrough
case 8:
    fmt.Println("The integer was <= 8")
    fallthrough
default:
    fmt.Println("default case")
}
//输出
//The integer was <= 6
//The integer was <= 7
//The integer was <= 8
//default case

函数

声明

声明格式如下

func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
    //这里是处理逻辑代码
    //返回多个值
    return value1, value2
}

从上面的代码可以看出

  1. 关键字func用来声明一个函数funcName
  2. 函数可以有一个或者多个参数,每个参数后面带有类型,通过,分隔
  3. 函数可以返回多个值
  4. 上面返回值声明了两个变量output1和output2,如果你不想声明也可以,直接就两个类型
  5. 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号
  6. 如果没有返回值,那么就直接省略最后的返回信息
  7. 如果有返回值, 那么必须在函数的外层添加return语句

下面一个简单代码示例

package main

import "fmt"

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func main() {
    a, b := 1, 7
    fmt.Printf("Max(%d,%d):%d", a, b, max(a, b))
}

多个返回值

Go语言比C更先进的特性,其中一点就是函数能够返回多个值。

package main
import "fmt"

//返回 A+B 和 A*B
func SumAndProduct(A, B int) (int, int) {
    return A+B, A*B
}

func main() {
    x := 3
    y := 4

    xPLUSy, xTIMESy := SumAndProduct(x, y)

    fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
    fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}

上面的例子我们可以看到直接返回了两个参数,当然我们也可以命名返回参数的变量,这个例子里面只是用了两个类型,我们也可以改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。但如果你的函数是导出的(首字母大写),官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。

func SumAndProduct(A, B int) (add int, Multiplied int) {
    add = A+B
    Multiplied = A*B
    return
}

变参

Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参:
func varArg(arg ...int){}
arg ...int告诉Go这个函数接受不定数量的参数(可以把int换为别的类型)。注意,这些参数的类型全部是int。在函数体中,变量arg是一个int的slice

image

传值与传指针

传值实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。

而传指针能更改参数的值,看下面代码

package main
import "fmt"

//简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,
    *a = *a+1 // 修改了a的值
    return *a // 返回新值
}

func main() {
    x := 3

    fmt.Println("x = ", x)  // 应该输出 "x = 3"

    x1 := add1(&x)  // 调用 add1(&x) 传x的地址

    fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
    fmt.Println("x = ", x)    // 应该输出 "x = 4"
}

变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有add1函数知道x变量所在的地址,才能修改x变量的值。所以我们需要将x所在地址&x传入函数,并将函数的参数的类型由int改为*int,即改为指针类型,才能在函数中修改x变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。

传指针有什么好处

  1. 传指针使得多个函数能操作同一个对象。
  2. 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
  3. Go语言中channel,slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)

defer

延迟(defer)语句,你可以在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。

下面是defer在打开文件的应用场景。
未使用defer

func ReadWrite() bool {
    file.Open("file")
// 做一些工作
    if failureX {
        file.Close()
        return false
    }

    if failureY {
        file.Close()
        return false
    }

    file.Close()
    return true
}

使用defer后,代码变得简洁

func ReadWrite() bool {
    file.Open("file")
    defer file.Close()
    if failureX {
        return false
    }
    if failureY {
        return false
    }
    return true
}

如果有很多调用defer,那么defer是采用后进先出模式,所以如下代码会输出4 3 2 1 0

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

函数作为值类型

Go中函数也是一种变量,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

如下面例子

package main
import "fmt"

type testInt func(int) bool // 声明了一个函数类型

func isOdd(integer int) bool {
    if integer%2 == 0 {
        return false
    }
    return true
}

func isEven(integer int) bool {
    if integer%2 == 0 {
        return true
    }
    return false
}

// 声明的函数类型在这个地方当做了一个参数

func filter(slice []int, f testInt) []int {
    var result []int
    for _, value := range slice {
        if f(value) {
            result = append(result, value)
        }
    }
    return result
}

func main(){
    slice := []int {1, 2, 3, 4, 5, 7}
    fmt.Println("slice = ", slice)
    odd := filter(slice, isOdd)    // 函数当做值来传递了
    fmt.Println("Odd elements of slice are: ", odd)
    even := filter(slice, isEven)  // 函数当做值来传递了
    fmt.Println("Even elements of slice are: ", even)
}

函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到testInt这个类型是一个函数类型,然后两个filter函数的参数和返回值与testInt类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。

Panic和Recover

Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panic和recover机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有panic的东西。这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢?
Panic

是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数F调用panic,函数F的执行被中断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。恐慌可以直接调用panic产生。也可以由运行时错误产生,例如访问越界的数组。

Recover

是一个内建的函数,可以让进入令人恐慌的流程中的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入恐慌,调用recover可以捕获到panic的输入值,并且恢复正常的执行。

Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
下面是一个例子

package main

import "fmt"

func main() {

    defer func() { // 必须要先声明defer,否则不能捕获到panic异常

        fmt.Println("c")

        if err := recover(); err != nil {

            fmt.Println(err) // 这里的err其实就是panic传入的内容,55

        }

        fmt.Println("d")

    }()     //匿名函数

    f()

}

func f() {

    fmt.Println("a")

    panic(55)

    fmt.Println("b")

    fmt.Println("f")

}

recover必须定义在panic之前的defer语句中。在这种情况下,当panic被触发时,该goroutine不会简单的终止,而是会执行在它之前定义的defer语句。
上面代码输出结果为

a
c
55
d

main、init函数

Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。

Go程序会自动调用init()和main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。
下面是代码示例
mypkg包,里面有两个init函数

package mypkg

import (
    "fmt"
)

var I int

func init() {
    I = 0
    fmt.Println("Call mypackage init1")
}

func init() {
    I = 1
    fmt.Println("Call mypackage init2")
}

main.go

package main

import (
    "fmt"
    mypkg "initFunc"
)

func main() {
    fmt.Println("Hello go.... I = ", mypkg.I)
}

上面代码中main函数不用介绍在所有语言中都一样,它作为一个程序的入口,只能有一个。init函数在每个package是可选的,可有可无,甚至可以有多个(但是强烈建议一个package中一个init函数),init函数在你导入该package时程序会自动调用init函数,所以init函数不用我们手动调用,l另外它只会被调用一次,因为当一个package被多次引用时,它只会被导入一次。
上面代码执行结果

Call mypackage init1
Call mypackage init2
Hello go.... I =  1

struct类型

struct声明

Go语言中,也和C或者其他语言一样,我们可以声明新的类型,作为其它类型的属性或字段的容器。
例如,我们可以创建一个自定义类型person代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型我们称之struct。如下代码所示:

type person {
    name string
    age int
}

使用结构体

type person struct {
    name string
    age int
}

var P person  // P现在就是person类型的变量了

P.name = "Astaxie"  // 赋值"Astaxie"给P的name属性.
P.age = 25  // 赋值"25"给变量P的age属性
fmt.Printf("The person's name is %s", P.name)  // 访问P的name属性.

另外的几种声明方式

  1. 按照顺序提供初始化值
    p := person{"miracle778",21}
  2. 按照field:value的方式初始化,这样可以任意顺序
    p:= person{age:17,name:"miracle778"}
  3. 通过new分配一个指针,此处的p类型为 *person
    p := new(person)

完整使用struct的例子

package main
import "fmt"

// 声明一个新的类型
type person struct {
    name string
    age int
}

// 比较两个人的年龄,返回年龄大的那个人,并且返回年龄差
// struct也是传值的
func Older(p1, p2 person) (person, int) {
    if p1.age>p2.age {  // 比较p1和p2这两个人的年龄
        return p1, p1.age-p2.age
    }
    return p2, p2.age-p1.age
}

func main() {
    var tom person

    // 赋值初始化
    tom.name, tom.age = "Tom", 18

    // 两个字段都写清楚的初始化
    bob := person{age:25, name:"Bob"}

    // 按照struct定义顺序初始化值
    paul := person{"Paul", 43}

    tb_Older, tb_diff := Older(tom, bob)
    tp_Older, tp_diff := Older(tom, paul)
    bp_Older, bp_diff := Older(bob, paul)

    fmt.Printf("Of %s and %s, %s is older by %d years\n",
        tom.name, bob.name, tb_Older.name, tb_diff)

    fmt.Printf("Of %s and %s, %s is older by %d years\n",
        tom.name, paul.name, tp_Older.name, tp_diff)

    fmt.Printf("Of %s and %s, %s is older by %d years\n",
        bob.name, paul.name, bp_Older.name, bp_diff)
}

struct的匿名字段

上面介绍了如何定义一个struct,定义的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。
当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。

如下面例子第11行,匿名字段为struct

package main
import "fmt"

type Human struct {
    name string
    age int
    weight int
}

type Student struct {
    Human  // 匿名字段,那么默认Student就包含了Human的所有字段
    speciality string
}

func main() {
    // 我们初始化一个学生
    mark := Student{Human{"Mark", 25, 120}, "Computer Science"}

    // 我们访问相应的字段
    fmt.Println("His name is ", mark.name)
    fmt.Println("His age is ", mark.age)
    fmt.Println("His weight is ", mark.weight)
    fmt.Println("His speciality is ", mark.speciality)
    // 修改对应的备注信息
    mark.speciality = "AI"
    fmt.Println("Mark changed his speciality")
    fmt.Println("His speciality is ", mark.speciality)
    // 修改他的年龄信息
    fmt.Println("Mark become old")
    mark.age = 46
    fmt.Println("His age is", mark.age)
    // 修改他的体重信息
    fmt.Println("Mark is not an athlet anymore")
    mark.weight += 60
    fmt.Println("His weight is", mark.weight)
}

上面代码,Student可以访问属性age和name,实现了字段的继承。

此外student还能访问Human这个字段作为字段名。如下面代码

mark.Human = Human{"Marcus", 55, 220}
mark.Human.age -= 1

所有的内置类型和自定义类型都是可以作为匿名字段的。请看下面的例子

package main
import "fmt"

type Skills []string    //Skills 用slice类型,可以通过append函数修改增加

type Human struct {
    name string
    age int
    weight int
}

type Student struct {
    Human  // 匿名字段,struct
    Skills // 匿名字段,自定义的类型string slice
    int    // 内置类型作为匿名字段
    speciality string
}

func main() {
    // 初始化学生Jane
    jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"}
    // 现在我们来访问相应的字段
    fmt.Println("Her name is ", jane.name)
    fmt.Println("Her age is ", jane.age)
    fmt.Println("Her weight is ", jane.weight)
    fmt.Println("Her speciality is ", jane.speciality)
    // 我们来修改他的skill技能字段
    jane.Skills = []string{"anatomy"}
    fmt.Println("Her skills are ", jane.Skills)
    fmt.Println("She acquired two new ones ")
    jane.Skills = append(jane.Skills, "physics", "golang")
    fmt.Println("Her skills now are ", jane.Skills)
    // 修改匿名内置类型字段
    jane.int = 3
    fmt.Println("Her preferred number is", jane.int)
}

当继承的字段名重复了的时候,最外层的优先访问,利用这个特性,可以实现重载功能。
如下例

package main
import "fmt"

type Human struct {
    name string
    age int
    phone string  // Human类型拥有的字段
}

type Employee struct {
    Human  // 匿名字段Human
    speciality string
    phone string  // 雇员的phone字段
}

func main() {
    Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
    fmt.Println("Bob's work phone is:", Bob.phone)
    // 如果我们要访问Human的phone字段
    fmt.Println("Bob's personal phone is:", Bob.Human.phone)
}

面向对象

上面结构体有匿名字段继承等操作,如果把函数当成struct的字段处理,就是面向对象。
函数的另一种形态,带有接收者的函数,我们称为method

method声明

method是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在func后面增加了一个receiver(也就是method所依从的主体)。
method声明方法如下
func (r ReceiverType) funcName(parameters) (results)

下面看个例子

package main
import (
    "fmt"
    "math"
)

type Rectangle struct {
    width, height float64
}

type Circle struct {
    radius float64
}

func (r Rectangle) area() float64 {     //接受者是Rectangle类型
    return r.width*r.height
}

func (c Circle) area() float64 {    //接受者是Circle类型
    return c.radius * c.radius * math.Pi
}


func main() {
    r1 := Rectangle{12, 2}
    r2 := Rectangle{9, 4}
    c1 := Circle{10}
    c2 := Circle{25}

    fmt.Println("Area of r1 is: ", r1.area())
    fmt.Println("Area of r2 is: ", r2.area())
    fmt.Println("Area of c1 is: ", c1.area())
    fmt.Println("Area of c2 is: ", c2.area())
}

使用method时注意几点

  1. 虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样
  2. method里面可以访问接收者的字段
  3. 调用method通过.访问,就像struct里面访问字段一样
image

在上例,method area() 分别属于Rectangle和Circle, 于是他们的 Receiver 就变成了Rectangle 和 Circle, 或者说,这个area()方法 是由 Rectangle/Circle 发出的。

值得说明的一点是,图示中method用虚线标出,意思是此处方法的Receiver是以值传递,而非引用传递,是的,Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作,下面对此会有详细论述。

receiver为指针

为了说明 指针作为receiver的用法,下面放一个复杂点例子

package main
import "fmt"

const(
    WHITE = iota
    BLACK
    BLUE
    RED
    YELLOW
)

type Color byte

type Box struct {
    width, height, depth float64
    color Color
}

type BoxList []Box //a slice of boxes

func (b Box) Volume() float64 {
    return b.width * b.height * b.depth
}

func (b *Box) SetColor(c Color) {
    b.color = c
}

func (bl BoxList) BiggestColor() Color {
    v := 0.00
    k := Color(WHITE)
    for _, b := range bl {
        if bv := b.Volume(); bv > v {
            v = bv
            k = b.color
        }
    }
    return k
}

func (bl BoxList) PaintItBlack() {
    for i, _ := range bl {
        bl[i].SetColor(BLACK)
    }
}

func (c Color) String() string {
    strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
    return strings[c]
}

func main() {
    boxes := BoxList {
        Box{4, 4, 4, RED},
        Box{10, 10, 1, YELLOW},
        Box{1, 1, 20, BLACK},
        Box{10, 10, 1, BLUE},
        Box{10, 30, 1, WHITE},
        Box{20, 20, 20, YELLOW},
    }

    fmt.Printf("We have %d boxes in our set\n", len(boxes))
    fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
    fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String())
    fmt.Println("The biggest one is", boxes.BiggestColor().String())

    fmt.Println("Let's paint them all black")
    boxes.PaintItBlack()
    fmt.Println("The color of the second one is", boxes[1].color.String())

    fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
}

上面代码的第25行,SetColor method的receiver是 Box,这里是因为要改变Box的属性,所以用*Box。那26行按道理应该是*b.color = c这样写,但是上面代码却写成了b.color = c,事实上,在Go语言中,两种写法都行。当你用指针去访问相应的字段时(虽然指针没有任何的字段),Go知道你要通过指针去获取这个值。
我们又看到43行的语句,在PaintItBlack里面调用SetColor的时候是不是应该写成(&bl[i]).SetColor(BLACK),因为SetColor的receiver是
Box,而不是Box。但示例代码写成了bl[i].SetColor(BLACK)。事实上,这两种写法都行。因为Go知道receiver是指针,他自动帮你转了。

也就是说

如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method

类似的

如果一个method的receiver是T,你可以在一个T类型的变量P上面调用这个method,而不需要 P去调用这个method

所以,你不用担心你是调用的指针的method还是不是指针的method,Go知道你要做的一切。
通俗讲就是,不管method声明的时候是func (var *struct) name() {}还是func (var struct) name() {},调用的时候var.name()可以通用。(个人理解,可能有误)

method继承

method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。
如下例

package main
import "fmt"

type Human struct {
    name string
    age int
    phone string
}

type Student struct {
    Human //匿名字段
    school string
}

type Employee struct {
    Human //匿名字段
    company string
}

//在human上面定义了一个method
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

func main() {
    mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
    sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}

    mark.SayHi()
    sam.SayHi()
}

method 重写

和上面struct匿名字段冲突一样的道理,我们可以在Employee上面定义一个method,重写了匿名字段的方法。

package main
import "fmt"

type Human struct {
    name string
    age int
    phone string
}

type Student struct {
    Human //匿名字段
    school string
}

type Employee struct {
    Human //匿名字段
    company string
}

//Human定义method
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

//Employee的method重写Human的method
func (e *Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone) //Yes you can split into 2 lines here.
}

func main() {
    mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
    sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}

    mark.SayHi()
    sam.SayHi()
}

规则

通过这些内容,我们可以设计出基本的面向对象的程序了,但是Go里面的面向对象是如此的简单,没有任何的私有、公有关键字,通过大小写来实现(大写开头的为公有,小写开头的为私有),方法也同样适用这个原则。


interface

什么是interface

简单的说,interface是一组method的组合,我们通过interface来定义对象的一组行为。
上面method重写例子中Student和Employee都能SayHi,虽然他们的内部实现不一样,但是那不重要,重要的是他们都能say hi

让我们来继续做更多的扩展,Student和Employee实现另一个方法Sing,然后Student实现方法BorrowMoney而Employee实现SpendSalary。
这样Student实现了三个方法:SayHi、Sing、BorrowMoney;而Employee实现了SayHi、Sing、SpendSalary。

上面这些方法的组合称为interface(被对象Student和Employee实现)。例如Student和Employee都实现了interface:SayHi和Sing,也就是这两个对象是该interface类型。而Employee没有实现这个interface:SayHi、Sing和BorrowMoney,因为Employee没有实现BorrowMoney这个方法。

interface类型

interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。详细的语法参考下面这个例子

type Human struct {
    name string
    age int
    phone string
}

type Student struct {
    Human //匿名字段Human
    school string
    loan float32
}

type Employee struct {
    Human //匿名字段Human
    company string
    money float32
}

//Human对象实现Sayhi方法
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

// Human对象实现Sing方法
func (h *Human) Sing(lyrics string) {
    fmt.Println("La la, la la la, la la la la la...", lyrics)
}

//Human对象实现Guzzle方法
func (h *Human) Guzzle(beerStein string) {
    fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}

// Employee重载Human的Sayhi方法
func (e *Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone) //此句可以分成多行
}

//Student实现BorrowMoney方法
func (s *Student) BorrowMoney(amount float32) {
    s.loan += amount // (again and again and...)
}

//Employee实现SpendSalary方法
func (e *Employee) SpendSalary(amount float32) {
    e.money -= amount // More vodka please!!! Get me through the day!
}

// 定义interface
type Men interface {
    SayHi()
    Sing(lyrics string)
    Guzzle(beerStein string)
}

type YoungChap interface {
    SayHi()
    Sing(song string)
    BorrowMoney(amount float32)
}

type ElderlyGent interface {
    SayHi()
    Sing(song string)
    SpendSalary(amount float32)
}

interface值

如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。
例如上面例子中,我们定义了一个Men interface类型的变量m,那么m里面可以存Human、Student或者Employee值

因为m能够持有这三种类型的对象,所以我们可以定义一个包含Men类型元素的slice,这个slice可以被赋予实现了Men接口的任意结构的对象,这个和我们传统意义上面的slice有所不同。
如下面例子

package main
import "fmt"

type Human struct {
    name string
    age int
    phone string
}

type Student struct {
    Human //匿名字段
    school string
    loan float32
}

type Employee struct {
    Human //匿名字段
    company string
    money float32
}

//Human实现SayHi方法
func (h Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

//Human实现Sing方法
func (h Human) Sing(lyrics string) {
    fmt.Println("La la la la...", lyrics)
}

//Employee重载Human的SayHi方法
func (e Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone)
    }

// Interface Men被Human,Student和Employee实现
// 因为这三个类型都实现了这两个方法
type Men interface {
    SayHi()
    Sing(lyrics string)
}

func main() {
    mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
    paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
    sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
    tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}

    //定义Men类型的变量i
    var i Men

    //i能存储Student
    i = mike
    fmt.Println("This is Mike, a Student:")
    i.SayHi()
    i.Sing("November rain")

    //i也能存储Employee
    i = tom
    fmt.Println("This is tom, an Employee:")
    i.SayHi()
    i.Sing("Born to be wild")

    //定义了slice Men
    fmt.Println("Let's use a slice of Men and see what happens")
    x := make([]Men, 3)
    //这三个都是不同类型的元素,但是他们实现了interface同一个接口
    x[0], x[1], x[2] = paul, sam, mike

    for _, value := range x{
        value.SayHi()
    }
}

空interface

空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。

空interface使用示例

// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a可以存储任意类型的数值
a = i
a = s

一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值

interface函数参数

interface的变量可以持有任意实现该interface类型的对象,这给我们编写函数(包括method)提供了一些额外的思考,我们是不是可以通过定义interface参数,让函数接受各种类型的参数
例如fmt.Println函数可以接受任意类型的数据输出,那是因为其源文件内部实现有这样一个定义

type Stringer interface {
     String() string
}

也就是说,任何实现了String方法的类型都能作为参数被fmt.Println调用

package main
import (
    "fmt"
    "strconv"
)

type Human struct {
    name string
    age int
    phone string
}

// 通过这个方法 Human 实现了 fmt.Stringer
func (h Human) String() string {
    return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years -  ✆ " +h.phone+"❱"
    //strconv里两个函数  Itoa,int to string ; Atoi string to int
}


func main() {
    Bob := Human{"Bob", 39, "000-7777-XXX"}
    fmt.Println("This Human is : ", Bob)
}

//该程序输出This Human is :  ❰Bob - 39 years -  ✆ 000-7777-XXX❱

现在我们再回顾一下前面的Box示例,你会发现Color结构也定义了一个method:String。其实这也是实现了fmt.Stringer这个interface,即如果需要某个类型能被fmt包以特殊的格式输出,你就必须实现Stringer这个接口。如果没有实现这个接口,fmt将以默认的方式输出。

注:实现了error接口的对象(即实现了Error() string的对象),使用fmt输出时,会调用Error()方法,因此不必再定义String()方法了。

interface变量存储的类型

我们知道interface的变量里面可以存储任意类型的数值(该类型实现了interface)。那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法:
1、 Comma-ok断言
Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。
如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。
如下例

package main
import (
    "fmt"
    "strconv"
)
type Element interface{}
type List [] Element
type Person struct {
    name string
    age int
}
//定义了String方法,实现了fmt.Stringer
func (p Person) String() string {
    return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}
func main() {
    list := make(List, 3)
    list[0] = 1 // an int
    list[1] = "Hello" // a string
    list[2] = Person{"Dennis", 70}
    for index, element := range list {
        if value, ok := element.(int); ok {
            fmt.Printf("list[%d] is an int and its value is %d\n", index,value)
        } else if value, ok := element.(string); ok {
            fmt.Printf("list[%d] is a string and its value is %s\n",index, value)
        } else if value, ok := element.(Person); ok {
            fmt.Printf("list[%d] is a Person and its value is %s\n",index, value)
        } else {
            fmt.Printf("list[%d] is of a different type\n", index)
        }
    }
}

不过这里用到多个if else,有点冗余。这里就可以用到另一种方法,switch

2、switch测试
把上面的例子用switch重写一下

package main
import (
    "fmt"
    "strconv"
)
type Element interface{}
type List [] Element
type Person struct {
    name string
    age int
}
//打印
func (p Person) String() string {
    return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}
func main() {
    list := make(List, 3)
    list[0] = 1 //an int
    list[1] = "Hello" //a string
    list[2] = Person{"Dennis", 70}
    for index, element := range list{
        switch value := element.(type) {
            case int:
                fmt.Printf("list[%d] is an int and its value is %d\n",index, value)
            case string:
                fmt.Printf("list[%d] is a string and its value is %s\n",index, value)
            case Person:
                fmt.Printf("list[%d] is a Person and its value is %s\n",index, value)
            default:
                fmt.Println("list[%d] is of a different type", index)
        }
    }
}

这里用到了 element.(type),需要强调的是:element.(type)语法不能在switch外的任何逻辑里面使用,如果你要在switch外面判断一个类型就使用comma-ok。

嵌入interface

如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。类似于struct里面的匿名字段
源码包container/heap里面有这样的一个定义

type Interface interface {
    sort.Interface //嵌入字段sort.Interface
    Push(x interface{}) //a Push method to push elements into the heap
    Pop() interface{} //a Pop elements that pops elements from the heap
}

我们看到sort.Interface其实就是嵌入字段,把sort包里的sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法:

type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less returns whether the element with index i should sort
    // before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

另一个例子就是io包下面的 io.ReadWriter ,它包含了io包下面的Reader和Writer两个interface:

// io.ReadWriter
type ReadWriter interface {
    Reader
    Writer
}

反射

反射是程序执行时检查其所拥有的结构。尤其是类型的一种能力。所谓反射就是能检查程序在运行时的状态。我们一般用到的包是reflect包。python中用hasattr方法实现。
go语言中的反射通过refect包实现,reflect包实现了运行时反射,允许程序操作任意类型的对象。
具体参考:https://www.cnblogs.com/wdliu/p/9222283.html

待更

并发

你可能感兴趣的:(Go web编程学习(一))