golang快速上手

2021/10/18 posted in  服务端
Tags:  #go

第一本go的书籍

2013年买的第一本go语言的书,至今还是比较好的入门书籍

距离现在好几年了,这两年go生态相对比较好,从而线上服务转去golang。
实际就是现代C语言,没有class面向对象,只有使用函数来做面向对象。

go常用命令

go get

go get http://github.com/123/abc
$GOPATH
获取组件

go build

bin/

go run

go mod

dep

module
如果Go的版本太低不能使用,建议将Go的版本升级到最新。
环境变量中可以增加GOPROXY=https://goproxy.io 这样没有梯子的情况下可以正确的加载相应的包文件。
环境变量GO111MODULE不要设置,如果已经增加了这个变量请务必设置为GO111MODULE=auto。
在项目的根目录下使用命令go mod init projectName。

go tool

go tool nm ./example

基本类型

类型 长度(字节) 默认值 说明
bool 1 false

byte 1 0 uint8
rune 4 0 Unicode Code Point, int32
int, uint 4或8 0 32 或 64 位
int8, uint8 1 0 -128 ~ 127, 0 ~ 255,byte是uint8 的别名
int16, uint16 2 0 -32768 ~ 32767, 0 ~ 65535
int32, uint32 4 0 -21亿~ 21亿, 0 ~ 42亿,rune是int32 的别名
int64, uint64 8 0

float32 4 0.0
float64 8 0.0
complex64 8

complex128 16

uintptr 4或8 以存储指针的 uint32 或 uint64 整数
array 值类型
struct 值类型
string "" UTF-8 字符串
slice nil 引用类型
map nil 引用类型
channel nil 引用类型
interface nil 接口
function nil 函数

数组与切片

数组

Don’t communicate by sharing memory; share memory by communicating.

var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}

深拷贝是指将值类型的数据进行拷贝的时候,拷贝的是数值本身,所以值类型的数据默认都是深拷贝。浅拷贝指的是拷贝的引用地址,修改拷贝过后的数据,原有的数据也被修改。 那么如何做到引用类型的深拷贝?也就是需要将引用类型的值进行拷贝。修改拷贝的值不会对原有的值造成影响。

// copy(目标切片,数据源)  深拷贝数据函数
s2 := []int{1, 2, 3, 4}
s3 := []int{7, 8, 9}

copy(s2, s3)        //将s3拷贝到s2中    
fmt.Println(s2)     //结果 [7 8 9 4]
fmt.Println(s3)     //结果 [7 8 9]

copy(s3, s2[2:])    //将s2中下标为2的位置 到结束的值 拷贝到s3中 
fmt.Println(s2)     //结果 [1 2 3 4]
fmt.Println(s3)     //结果 [3 4 9]

copy(s3, s2)        //将s2拷贝到s3中
fmt.Println(s2)     //结果 [1 2 3 4]
fmt.Println(s3)     //结果 [1 2 3]

二维数据

数组长度是固定的,如果要扩展数组,得使用切片

切片

make()是Go语言中的内置函数,主要用于创建并初始化slice切片类型,或者map字典类型,或者channel通道类型数据。他与new方法的区别是。new用于各种数据类型的内存分配,在Go语言中认为他返回的是一个指针。指向的是一个某种类型的零值。make 返回的是一个有着初始值的非零值。

使用make函数

var s3 []int = make([]int, 0)

or

var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[start:end] 
1:3
var slice1 []int = arr[:end]        
var slice2 []int = arr[start:]        
var slice3 []int = arr[:] 
var slice4 = arr[:len(arr)-1]

切片是可索引的,并且可以由 len() 方法获取长度。
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
切片是面向地址的,修改数值会影响相关变量。

map

定义语法

map[KeyType]ValueType
    
//1, 声明map 默认值是nil
var m1 map[key_data_type]value_data_type
声明  变量名称 map[key的数据类型]value的数据类型
//2,使用make声明
m2:=make(map[key_data_type]value_data_type)
//3,直接声明并初始化赋值map方法
m3:=map[string]int{"语文":89,"数学":23,"英语":90}

demo:

func main() {
    scoreMap := make(map[string]int, 8)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    fmt.Println(scoreMap)
    fmt.Println(scoreMap["小明"])
    fmt.Printf("type of a:%T\n", scoreMap)
}


结构体

基本上和C的struct一样
在Go语言中,使用type关键字可以定义出新的自定义类型,有了自定义类型之后我们就可以为自定义类型添加各种方法了。

type person1 struct {
        name, city string
        age        int8
}

package main

import (
    "fmt"
)

//定义结构体
type Person struct {
    name    string
    age     int
    sex     string
    address string
}

实例:

func main() {
    //实例化后并使用结构体
    p := Person{} //使用简短声明方式,后面加上{}代表这是结构体
    
    p.age = 2     //给结构体内成员变量赋值
    p.address = "陕西"
    p.name = "好家伙"
    p.sex = "女"
    
    fmt.Println(p.age, p.address, p.name, p.sex)//使用点.来访问结构体内成员的变量的值。

}

指针

基本上和C一致
指针是存储另一个变量的内存地址的变量。 例如: 变量B的值为100, 地址为0x1122。变量A的值为变量B的地址0x1122,那么A就拥有了变量B的地址,则A就成为指针。Go语言中通过&获取变量的地址。通过* 获取指针所对应的变量存储的数值。

package main

import (
    "fmt"
)

func main() {

    //定义一个变量
    a := 2
    fmt.Printf("变量A的地址为%p", &a) //通过%p占位符, &符号获取变量的内存地址。
    //变量A的地址为0xc000072090
    

    //创建一个指针
    // 指针的声明 通过 *T 表示T类型的指针
    var i *int     //int类型的指针
    var f *float64 //float64类型的指针
    fmt.Println(i) // < nil >空指针
    fmt.Println(f)

    //因为指针存储的变量的地址 所以指针存储值
    i = &a
    fmt.Println(i)  //i存储a的内存地址0xc000072090
    fmt.Println(*i) //i存储这个指针存储的变量的数值2
    *i = 100
    fmt.Println(*i) //100
    fmt.Println(a)  //100通过指针操作 直接操作的是指针所对应的数值

}

指针的指针,也就是存储的不是具体的数值了,而是另一个指针的地址。

func main(){
    a := 2
    var i *int         //声明一个int类型的指针
    fmt.Println(&a)    //0xc00000c1c8
    i = &a             //将a的地址取出来放到i里面
    fmt.Println(&i)    //0xc000006028
    var a2 **int       //声明一个指针类型的指针
    a2 = &i            //再把i的地址放进a2里面
    fmt.Println(a2)    //获取的是a2所对应的数值0xc000006028也就是i的地址
}

指针属于引用类型的数据, 所以在传递过程中是将参数的地址传给函数,将指针作为参数传递时,只有值类型的数据,需要传递指针,而引用类型的数据本身就是传递的地址,所以数组传递可以使用指针,切片是引用类型数据,则不需要传递指针传递。

package main

import (
    "fmt"
)

func main() {
    s := 10
    fmt.Println(s) //调用函数之前数值是10
    fun1(&s)
    fmt.Println(s) //调用函数之后再访问则被修改成2
}

//接收一个int类型的指针作为参数
func fun1(a *int) {
    *a = 2
}

使用结构体调用指针

//1 使用结构体指针
var p *Person
p = &p2 //将p2 的地址赋给p
import "encoding/json"
package main

import (
    "encoding/json"
    "fmt"
)
//结构体
type Prescription struct {
    Name     string        
    Unit     string        
    Additive *Prescription 
}

func main() {
    p := Prescription{}
    p.Name = "鹤顶红"
    p.Unit = "1.2kg"
    p.Additive = &Prescription{
        Name: "砒霜",
        Unit: "0.5kg",
    }

    buf, err := json.Marshal(p) //转换为json返回两个结果
    if err != nil {
        fmt.Println("err = ", err)
        return
    }

    fmt.Println("json = ", string(buf))
}

package main

import (
    "encoding/json"
    "fmt"
)

//结构体
type Prescription struct {
    Name     string        `json:"name"` //重新指定json字段为小写输出
    Unit     string        `json:"unit"`
    Additive *Prescription `json:"additive,omitempty"`
}

func main() {
    jsonstr := `{"name":"鹤顶红","unit":"1.2kg","additive":{"name":"砒霜","unit":"0.5kg"}}`
    var p Prescription
    if err := json.Unmarshal([]byte(jsonstr), &p); err != nil {
        fmt.Println(err)
    }
    fmt.Println(p)
}

流程控件

日常语言的if、for range、switch不作具体描述,比较特殊的用法是select
while(true)
{

}
select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

select 是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。 select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s);
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}

函数与方法

如果你想让一个方法可以被别的包访问的话,你需要把这个方法的第一个字母大写。这是一种约定。

面向对象

package main

import (
    "fmt"
)

type Phone interface {
    call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    var phone Phone

    phone = new(NokiaPhone)
    phone.call()

    phone = new(IPhone)
    phone.call()

}

反射工具类

reflect

通道

通道(channel)是用来传递数据的一个数据结构。

//通道的声明
var channel chan int
//如果通道时nil 则要通过make创建通道
channel= make(chan int)

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

在函数或者方法前面加上关键字go,就会同时运行一个新的goroutine。

与函数不同的是goroutine调用之后会立即返回,不会等待goroutine的执行结果,所以goroutine不会接收返回值。 把封装main函数的goroutine叫做主goroutine,main函数作为主goroutine执行,如果main函数中goroutine终止了,程序也将终止,其他的goroutine都不会再执行。

虽然说Go编译器将Go的代码编译成本地可执行代码。不需要像java或者.net那样的语言需要一个虚拟机来运行,但其实go是运行在runtime调度器上的,它主要负责内存管理、垃圾回收、栈处理等等。也包含了Go运行时系统交互的操作,控制goroutine的操作,Go程序的调度器可以很合理的分配CPU资源给每一个任务。

Go1.5版本之前默认是单核执行的。从1.5之后使用可以通过runtime.GOMAXPROCS()来设置让程序并发执行,提高CPU的利用率。

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    //获取当前GOROOT目录
    fmt.Println("GOROOT:", runtime.GOROOT())
    //获取当前操作系统
    fmt.Println("操作系统:", runtime.GOOS)
    //获取当前逻辑CPU数量
    fmt.Println("逻辑CPU数量:", runtime.NumCPU())

    //设置最大的可同时使用的CPU核数  取逻辑cpu数量
    n := runtime.GOMAXPROCS(runtime.NumCPU())
    fmt.Println(n) //一般在使用之前就将cpu数量设置好 所以最好放在init函数内执行

    //goexit 终止当前goroutine
    //创建一个goroutine
    go func() {
        fmt.Println("start...")
        runtime.Goexit() //终止当前goroutine
        fmt.Println("end...")
    }()
    time.Sleep(3 * time.Second) //主goroutine 休眠3秒 让子goroutine执行完
    fmt.Println("main_end...")
}

延迟函数

package main

import (
    "fmt"
)

func main() {
    defer test(1) //第一个被defer的,函数后执行
    defer test(2) //第二个被defer的,函数先执行
    test(3)       //没有defer的函数,第一次执行
    
    //执行结果
    //3
    //2
    //1
}

func test(s int) {
    fmt.Println(s)
}

错误处理

实际每个函数都可以定义二个参数的返回,error类型
例如
txt, err = test("test.txt");

错误类型

package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("错误信息...")
    fmt.Println(err)

    num, err2 := Calculation(0)
    fmt.Println(num, err2)
}

//通过内置errors包创建错误对象来返回
func Calculation(divisor int) (int, error) {
    if divisor == 0 {
        return 0, errors.New("错误:除数不能为零.")
    }
    return 100 / divisor, nil
}
panic和recover
中断信息,recover接收信息

package main

import (
    "fmt"
)

func main() {
    Test1()
}

func Test1() {
    defer func() {
        ms := recover()//这里执行恢复操作
        fmt.Println(ms, "恢复执行了..") //恢复程序执行,且必须在defer函数中执行
    }()
    defer fmt.Println("第1个被defer执行")
    defer fmt.Println("第2个被defer执行")
    for i := 0; i <= 6; i++ {
        fmt.Println(i);
        if i == 4 {
            panic("中断操作") //让程序进入恐慌 终端程序操作
        }
    }
    
    defer fmt.Println("第3个被defer执行") //恐慌之后的代码是不会被执行的
}

常用工具包

官方常用工具包有如下
fmt、strings、strconv、os、io、errors

导入包

引入每个工具包,都先要先导入
使用import 导入包。go自己会默认从GO的安装目录和GOPATH环境变量中的目录,检索src下的目录进行检索包是否存在。所以导入包的时候路径要从src目录下开始写。GOPATH 就是我们自己定义的包的目录。

// 每行一个写法
import fmt
// 导入多个,还可以重命名
import (
    "fmt"
    "sync"
    "os" "test"
    _ "test.com/test"  // 只调用里面的init,不调用里面的方法
)

闭包

闭包:
package main
import (
    "fmt"
)
func main() {
    res := closure()
    fmt.Println(res) //0x49a880  返回内层函数函数体地址
    r1 := res()      //执行closure函数返回的匿名函数
    fmt.Println(r1)  //1
    r2 := res()
    fmt.Println(r2) //2 
    //普通的函数应该返回1,而这里存在闭包结构所以返回2 。
    //一个外层函数当中有内层函数,这个内层函数会操作外层函数的局部变量,并且外层函数把内层函数作为返回值,则这里内层函数和外层函数的局部变量,统称为闭包结构。这个外层函数的局部变量的生命周期会发生改变,不会随着外层函数的结束而销毁。
    //所以上面打印的r2 是累计到2 。

    res2 := closure() //再次调用则产生新的闭包结构 局部变量则新定义的
    fmt.Println(res2)
    r3 := res2()
    fmt.Println(r3)
}

//定义一个闭包结构的函数 返回一个匿名函数
func closure() func() int { //外层函数
    //定义局部变量a
    a := 0 //外层函数的局部变量
    //定义内层匿名函数 并直接返回
    return func() int { //内层函数
        a++ //在匿名函数中将变量自增。内层函数用到了外层函数的局部变量,此变量不会随着外层函数的结束销毁
        return a
    }
}

sync工具

package main

import (
    "fmt"
    "sync"
)

//创建一个同步等待组的对象
var wg sync.WaitGroup

func main() {
    wg.Add(3) //设置同步等待组的数量
    go Relief1()
    go Relief2()
    go Relief3()
    wg.Wait() //主goroutine进入阻塞状态
    fmt.Println("main end...")
}

func Relief1() {
    fmt.Println("func1...")
    wg.Done() //执行完成 同步等待数量减1
}
func Relief2() {
    defer wg.Done()
    fmt.Println("func2...")
}
func Relief3() {
    defer wg.Done() //推荐使用延时执行的方法来减去执行组的数量
    fmt.Println("func3...")
}

互斥锁

互斥锁,当一个goroutine获得锁之后其他的就只能等待当前goroutine执行完成之后解锁后才能访问资源。对应的方法有上锁Lock()和解锁Unlock()。

读写锁

互斥锁是用来控制多个协程在访问同一个资源的时候进行加锁控制,保证了数据安全,但同时也降低了性能,如果说多个goroutine同时访问一个数据,只是读取一下数据,并没有对数据进行任何修改操作,那么不管多少个goroutine来读取都应该是可以的。主要问题在于修改。修改的数据就需要加锁操作,来保证数据在多个goroutine读取的时候统一。 读取和读取之间是不需要互斥操作的,所以我们用读写锁专门针对读操作和写操作的互斥锁。