Go语言入门存档

Posted by xnchen on December 19, 2020

[TOC]

环境安装


max osx 10.15.6

  1. brew install go

  2. go version 查看go语言版本:


(base) xnchen@MacbookAir:~/Desktop/code/go-workspace/gopl.io/ch1/helloworld$ go version                                                         
go version go1.15.5 darwin/amd64

Tutorial


Go程序设计语言

基础知识


Go mod


go modules是golang1.11新加的特性。

我们使用go mod来管理项目

  1. 初始化项目

    mkdir test_pj
    cd test_pj
    go mod init test_pj
    

    会自动生成go.mod程序,在程序内部会有各类执行命令,例如:

    module test_pj // 指定模块名称
    require github.com/gin-gonic/gin v1.6.3	// 通过在go文件里的import,会自动生成来指定依赖项模块
    

    项目上传到github上就可以直接被其他人使用

  2. 版本控制

    使用git tag进行版本控制:

    // making a release
    git tag v1.0.0
    git push --tags
    

工作区结构


{workspace}
    src
        {module_name}
            {source_name}.go
    bin
        {module_name}

在同一个{module_name}文件夹内的.go文件中首行的package <包名>必须相同,建议和{module_name}同名

文件中import的是包文件夹名

这个好像已经被包管理取代了: 注意,包引用依赖(import package)的寻找过程只会按顺序寻找下列路径:

  1. {GOROOT}/src/{package name}
  2. {GOPATH}/src/{package name}
  3. {GOPATH}/src/{project name}/vendor/{package name}
  4. {GOPATH}/src/{project name}/{package name}

包(package)


每个源文件的开头使用package声明文件属于哪个包,范例中属于geometry包

package之后使用import声明导入其它包

package main是一个特殊的包,定义了一个独立的可执行文件。

包中的main函数是特殊的,程序开始编译的时候会搜索文件夹内含有main函数的文件,作为入口。

eg:

package geometry

import (
    "fmt"
    "os"
    "github.com/gin-gonic/gin"
)

func main() {
    var s, sep, string
    for i := 1; i < len(os.Args); i++ { 
        s += sep + os.Args[i]
        sep = " "
    }
    fmt.Println(s)
}
  • 在被导出的包中,其他包只能访问他们里面大写字母开头的变量和函数

  • 导入包必须使用,不使用会报错

    • 如果导入包不使用,可以用空白标识符_避免错误:

      import(
      	_ "geometry/rectangle"
      )
      
  • 可以使用别名alias:

    import(
    	util "project1/src/util"
    )
    // 这样就可以用util.<函数名>引用了
    

运行


  • 在包文件内创建一个go.mod文件表示这是一个go包:go mod init {包名}
  • 如果在程序中引用别的包,在运行时需要在go.mod中加入依赖。
    • 直接用go install 或者go build命令行会提醒你用go get {...}做。go get没有二进制安装功能,仅用于编辑go.mod
    • go mod tidy 可以用于删掉无用的模块,也可以用于将未导入的模块写入go.mod
    • 同时会生成一个go.sum文件,详细罗列当前项目依赖的所有模块版本。这个文件也是需要提交的
  • 运行go程序
    • go install {package name}:将包生成可执行二进制文件,二进制文件的安装路径是{GOPATH}/bin/{package name}
      • 如果{package name}空置,则默认为当前目录的包,等同于go install .
      • 拉取的依赖缓存会自动安装在``{GOPATH}/pkg`中
    • go run {.go文件工作区路径}:编译并运行。不输出二进制文件,直接执行源码的main()函数。
    • go build:搜索当前目录的go源码,然后编译文件,生成当前目录名的可执行文件并放置在当前目录下。
    • 简单程序用go playground可以运行
  • 依赖
    • 如果有来自github之类的包,用 go get {包名}获得,例如 go get github.com/bitly/go-simplejson
      • 这样下载的包会自动安装在{GOPATH}/pkg
      • 并且自动更新go.mod文件

变量初始化


// 1 声明了变量s和sep是string类型
var s, sep string
// 2 简短声明,只适用于函数内部使用
s, sep := "", ""
// 3 比较冗余的方法
var s2 int64 = 1
// 4 go的自动推断,可以根据初始值自动推断变量类型
var s3 = 29
// 5 声明多个变量
var (
 name = "dot"
  age = 29
  height int
)

如果变量没有明确初始化值,将会隐式地初始化为该类型空值

常量


const a int = 2
const a = 55

常量不能重新赋值为其他值,需要在编译的时候就确定值,因此下面这种会出错:

const b = math.Sqrt(4) // 右式在运行时计算

另外,常量无类型

类型


Go是强类型语言,因此在不同类型之间的操作需要显式的类型转换

有符号整型:int int8 int16 int32/rune int64

无符号整型:uint uint8/byte uint16 uint32 uint64

浮点型:float32 float64

复数整型:complex64 complex128

// 内建函数complex()用于创建复数,实部和虚部要是相同类型,float32/float64:
c1 := complex(5, 7)
// 或者
c1 := 6+7i

字符串:string

布尔类型:bool

自定类型:

type typeName typeLiteral //语法

// 例子:
type months map[string]int
//其实就像是假名一样

递增语句


i++
i--

不像C语言那样是表达式,在go中j=i++是不合法的

循环语句


for是go中唯一的循环语句(go没有while或者do while

// 基础的for
for initialization; condition; post {
    // statement
}
// 例子
for i:=1; i<=10; i++ {	// 如果是不需要赋初值的i也可以写成: for ;i<10;i++ {...}
  // statement
} 

for中的三部分都是可以省略的:

// while循环
for condition {
    // ...
}
// while(1)
for {
    // ...
}

另一种for循环在字符串或slice数据上迭代:

for _, args := range os.Args[1:] {
    // ...
}

range产生一对值:索引和索引处元素值

使用空标识符_来丢弃无用的索引

break: 终止for循环

continue: 跳出当前循环,并继续下一循环

switch语句


条件语句,将表达式的值与可能匹配的选项列表进行比较

finger := 4
switch finger {
case 1, 3, 4:
    fmt.Println("thunmb")
case 2:
    fmt.Println("Index")
default:
    fmt.Println("warning")
}

如果省略switch后面的表达式,表示所有的case后面的statement都会进行匹配

num := 75
switch {
    case num>=0 && num<50: 
    	fmt.Println('1')
    case num >=50 && num<100: 
    	fmt.Println('2')
}

fallthrough语句:默认是如果执行了case就会从switch中跳出,在case中加入fallthrough可以在执行完case之后继续顺序的下一个case(不判断条件),然后下一个case如果没有fallthrough就会跳出

fallthrough不能放在default中,会报错。

Join函数


import (
    "string"
    "os"
}

strings.Join(os.Args[1:], " ")

print函数


import "fmt"

a := []byte("test")
fmt.Println(a)     // 输出:[116 101 115 116] 即使用默认的类型格式显示
fmt.Printf("%s", a)      // 输出:test

s = fmt.Sprintf("%v", a) // 使用格式化规则(即%v)对传入的字符串a格式化,并返回格式化后的结果
fmt.Fprintf(os.Stderr, "%s", a) // 格式化传入字符串a,并将其输出到第一个输入变量规定的的io.Writers类型对象中

if-else语句


if condition{
 	// statement
}	else if {
    // statement
} else {
    // st2atement
}

else得在if后面的右括号的同一行内

字符串处理



import {
    "strconv"
    "strings"
}

// ===== 类型转换 ========== 
var num = 1
s := strconv.Itoa(num) // strconv.Itoa() 数字到字符串

i1, err := strconv.Atoi(s) // 字符串到数字 err是错误信息

// ====== 去除string前后空格 =====
buf := make([]byte) {  b  b }
fmt.Printf("%s",strings.TrimSpace(string(buf))) // 输出"b  b",即去除了前后空格

数组


数组是同一类型元素的集合,是值类型而不是引用类型

声明方式:

// 长度为3的int数组
var a[3]int 
// 简略声明
a := [3]int{12, 78, 50}
// 不全赋值的简略声明
a := [3]int{12}
// 自动识别长度
// 如果...出现在数组长度的位置,那么数组长度由初始化数组的元素个数决定
a := [...]int{12, 78, 50}
// 使用index:value方式定义的数组
var d = [...] int{1, 2, 4:5, 6} // [1,2,0,0,5,6]
// 多维数组
a:= [3][2]string{
    {"1", "2"},
    {"3", "4"},
    {"5", "6"},	// 这个最后的逗号还一定得加!
}
  • len函数:len(a)得到数组长度

  • range:迭代数组 for i, v := range a

切片


创建方法

i := []int {1,2,3}
i := []int {}
i := make([]int, len, cap)
i := []int {
    1,
    2,
    3,
}

可以看做是下面的结构体类型表示:

type slice struct{
	Length			int
    Capacity		int
    ZerothElement 	*byte
}
  • 长度动态
  • []int/[]string←类型名长这样
  • 逻辑类似python,a[start:end]包含start不包含end,a[:]是全部a的值
  • 切片自己不含有任何数据,是引用类型?eg:
a := [...]int{1,2,3,4,5}	// 数组a
b := a[0:3]					// a的切片
for i:= range b { 
	b[i]++ 
}
fmt.Printlb(a) // 会发现a的值被改变了
  • 切片的长度(len(b))是切片元素数,切片的容量(cap(b))是从切片索引开始的底层数组中元素数
  • append函数:将新元素追加到切片

    • b=append(b, 3, 4, 5)将3、4、5追加到切片b中(追加元素数可变)
    • 实际上是创建新数组,将切片内元素复制到新数组,并返回这个新数组的切片引用
  • 创建切片的方法:
    • make函数:i := make([]int, len, cap)创建切片,容量cap是可选参数,默认值为长度len
    • i:=[]int {1,2,3}也可以
  • copy函数:copy(a, b)将切片b copy给切片a,切片a的底层数组是新的(程序的垃圾回收机制可以将比较大的b的底层数组回收)
  • 在函数处理数组的话,就往函数里传入切片

可变参数函数


如果函数最后一个参数被记为...T,这时函数可以接受人一个T类型参数作为最后一个参数

func find(num int, nums ...int){
    ...
}
// 在这个函数中,nums被当做整形切片`[]int`
// 如何直接将整型切片当做最后一个参数传入上述find函数?
// 语法糖:
nums := []int{89,59,30}
find(89, nums...) 	// 在切片后加上...后缀

map


值和键关联的内置类型,是引用类型

创建方法:

// 键key是string类型,值是int类型
personSalary := make(map[string]int)
personSalary["steve"] = 12000

// 或者在声明的时候赋值
personSalary := map[string]int {
    "steve": 12000,
    "jamie": 15000,
}

// 其他的创建方法
cacheNode:= make(map[*Node]*Node) // 一个哈希表,key是指向Node类型的指针,value也是指向Node类型的指针
var cacheNode map[*Node]*Node

获取元素的方法:

key1 = personSalary["steve"] // 如果steve不存在则会返回值类型的零值
key2, ok = personSalary["steve"] // 这种方法的第二个参数代表了map中是否存在这个键

for key, value := range key1 {
    
}

map无法相互比较

go语言中,可比较的类型(支持==和!=)都可以作为map的key,因此除了slice、map、function这几种类型,其他类型都是ok的。任何值都可以作为value,包括map类型。

map每次扩容会增加到上次大小的两倍。

从性能考虑,在初始化map时,指明map的容量。

字符串


s := "hello world"

字符串是不可变的,为了修改字符可以把字符串转变成rune切片,然后切片进行修改,再转换成字符串

s := "hello world"
temp := []rune(s)
temp[0] = 'a'
result := string(temp)

指针


var a *int // 	指针的创建 初始不赋值的话就是nil
b := 255
a = &b 		// 	指针a指向b的地址
fmt.Println(*a) // 打印出指针a指向的地址的值

Go不支持其他指针运算(比如指针a++之类的

狂喜

结构(structure)


// 结构体的声明
type Employee struct {
    firstName, lastname 	string
    age						int
}

// 结构体的使用
emp1 := Employee{
    firstName: "Sam",
    age: 25,
}

emp2 := Employee{"Tom", 29}

// 访问结构体
fmt.Println(emp1.firstName)

// *******匿名字段*********
type Employee struct {
    identity
    age			int
}
type identity struct {
    id			int
    home		string
}
// 初始化
emp3 := Employee {identity{10, "Xiamen"}, 20} // 初始化需要使用Struct名

// 可以从上一层Employee的字段名称访问identity中字段的值
fmt.Println(emp3.home)

// 多个struct一起声明
type(
    StructName1 struct{
        fisrtName 	string
        age			int
    }
    StructName2 struct{
        IndexType	string
        In
    }
)

// struct的field tag 
// 可以添加该tag来快速地转换json和go结构体
type TestStruct struct {
	Bit64 int64  `ddf:"idx=k1"`
	Bytes []byte `ddf:"idx=k2"`
}

结构体是值结构,如果每个字段是可比较的,则该结构体可比较。

函数


func calculate(x int, y int) int{
  ...
}
// 多返回值
func calculate(x, y int)(float64, float64){
  ...
}
// 命名返回值
func calculate(x, y int)(area, perimeter float64){
  area = x * y
  perimeter = (x + y) * 2
  return // 不需要明确指定返回量,因为在函数第一行声明了返回的变量
}
/// 匿名函数
f := func(args string) {
    fmt.Println(args)
}
f("hello world")
// 或者
func(args string) {
    fmt.Println(args)
} ("Hello world")

方法(method)


GO中用于实现类型的骚操作 语法如下

func (r ReceiverType) funcName(parameters) (results)

例子:

type Rectangle struct{
    width, heigh float64
} 
type Circle struct {
    radius float64
}

func (r Rectangle) area (a int, b int) float64 {
  ...
}// 分别是:r:接收器(实现了该方法的对象),a、b方法的输入参数,float64返回类型

x := Rectangle{1,2}
x.calculate(3,4) // 这样使用方法

方法可以设置接收的对象(receiver),可以是值或者指针,如果是指针接收器,方法内部的改变对调用者可见。

  • 如果一个方法的接收器是指针*T,可以直接在T类型的实例上调用这个方法,不需要用&V去调用
  • 如果一个方法的接收器是值T,可以在这个T类型的实例V上调用,也可以在T类型的指针&V上调用

所以不需要担心到底是指针的方法还是值的方法,用就是了(

接口(Interface)


用于实现继承、多态的骚操作。

可以把接口看做是内部的一个元组(type, value),包含了底层值(underlying value)和具体类型(concrete type)。只要实现了接口内定义的所有方法,就可以被接口包含。

即:接口指定了一个类型该具有的方法。

例子:

type VowelsFinder interface {
    FindVowels() []rune
}

空接口表示为interface{},所有类型都实现了空接口(可以认为所有类型都有父类型inferface{}吗?)

函数类型(Function types)


一个函数类型中,所有的函数都有相同的输入和输出类型

type Greeting func(name string) string 
// Greeting函数类型中,所有的函数都是输入string,输出string

func english(name string) string {
    return "Hello, "+name
}
func french(name string) string {
    return "bonjour, "+name
}''

自增长的iota


const (
	a int	=	iota	//这个int也可以不加 a=0
    b					//b=1
    c					//c=2
    _					// 这里会被跳过
    d					//d=4
)
//iota也可以写成表达式,比如 1<<iota 就是每行左移一位

并行


Go语言通过Goroutine和channel来实现并行

go func() // 启动一个goroutine

// 声明channel的方法
var a chan int
// 或者简短声明
a := make(chan int) 
// 单向channel:唯送信道 send only channel
a := make(chan<- int)
// 关闭信道
close(a)
// buffered channel
a := make(chan int, 4) // 在信道内对象没有大于其容量时不会发生阻塞。无缓冲信道的容量为0

channel的阻塞:

a <- 3 // 写数据
<- a // 读数据 这里的接收端空置,合法
s1, s2 := <-a, <-b // s1和s2从channel a和b读数据
// 当goroutine向channel写数据时,程序会在发送数据语句处阻塞,直到有其他goroutine读取了chennel数据
// 同理,当goroutine读数据时,如果没有其他goroutine把数据写入信道,那么读取过程也会阻塞

双向通道chan T的值可以被隐式转换为单向通道类型chan<- T<-chan T,但反之不行(即使显式也不行)。 类型chan<- T<-chan T的值也不能相互转换。

  • 向一个已关闭的channel发送元素会引起panic

  • 在发送端关闭Channel,接收端还会继续接收到通道中的元素

  • select…case… 尝试从close的channel接受数据会直接获取该channel类型的空值

defer


defer关键字会在当前函数或者方法返回之前执行传入的函数

构造函数


实际上go没有构造函数这种东西

可以参考 链接 来实现构造函数

make和new


  • make 的作用是初始化内置的数据结构,也就是我们在前面提到的切片、哈希表和 Channel;
  • new 的作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针;

我们在代码中往往都会使用如下所示的语句初始化这三类基本类型,这三个语句分别返回了不同类型的数据结构:

slice := make([]int, 0, 100)
hash := make(map[int]bool, 10)
ch := make(chan int, 5)

Go

  1. slice 是一个包含 datacaplen 的结构体 reflect.SliceHeader
  2. hash 是一个指向 runtime.hmap 结构体的指针;
  3. ch 是一个指向 runtime.hchan 结构体的指针;

相比与复杂的 make 关键字,new 的功能就简单多了,它只能接收类型作为参数然后返回一个指向该类型的指针:

i := new(int)

var v int
i := &v

单元测试


通过go test命令,可以自动执行如下形式的任何函数

func TestXxx(*testing.T)

注意:Xxx 可以是任何字母数字字符串,但是第一个字母不能是小写字母。

在这些函数中,使用 ErrorFail 或相关方法来发出失败信号。

要编写一个新的测试套件,需要创建一个名称以 _test.go 结尾的文件,该文件包含 TestXxx 函数,如上所述。 将该文件放在与被测试文件相同的包中。该文件将被排除在正常的程序包之外,但在运行 go test 命令时将被包含。 有关详细信息,请运行 go help testgo help testflag 了解。

go test -bench=funcName用于基准测试(执行几万次)某个函数funcName,函数名字为Benchma_funcName

代码覆盖测试:go test -converprofile=c.out

查看覆盖率报告:go tool cover -html=c.out