Golang从入门到放弃

安装:

1.CentOS7 安装Go

下载tar包:Downloads - The Go Programming Language,自行选择合适的版本,比如Linux x86-64

解压到默认路径:

tar -zxvf go1.15.12.linux-amd64.tar.gz  -C /usr/local/

添加环境变量到/etc/profile文件:

export GOROOT=/usr/local/go
export GOPATH=/home/go/GOPATH
export PATH=$PATH:$GOROOT/bin:$GOROOT/bin:$GOPATH/bin:$GOPATH/bin:
export GOPROXY=https://goproxy.cn
go env -w GO111MODULE=auto

一些第三方库依赖gcc,安装gcc:

yum install gcc

go get用来下载第三方库,依赖git,安装git:

yum -y install git

go get有时候会失败:git 推送出现 "fatal: The remote end hung up unexpectedly" 解决方案_Arviiin的博客-CSDN博客

参考书籍:Go语言编程

0X00 入门

每个Go源代码文件的开头都是一个package声明,表示该Go代码所属的包。包是Go语言里最基本的分发单位,也是工程管理中依赖关系的体现。要生成Go可执行程序,必须建立一个名字为main的包,并且在该包中包含一个叫main()的函数(该函数是Go可执行程序的执行起点)。

package main

import "fmt"// 我们需要使用fmt包中的Println()函数

func main() {
    fmt.Println("Hello, world. 你好,世界!")
}

关于import:

如果一个package main 中导入其他的包,包将被顺序导入;
如果导入的包中依赖其他包(包B),会首先导入B包,然后初始化B包中常量和变量,最后如果B包中有init()方法,会自动执行init();
所有包导入完成后才会对main中的常量和变量进行初始化,谈后执行main中的init函数(如果存在),最后执行main函数;
如果一个包被导入多次则该包只会被导入一次;

有时包名很长,每次都要通过包名.func_name去调用感觉很冗长,所以可以给包名起个别名,例如:

package controllers

import (
	beego "github.com/beego/beego/v2/server/web"
)

type MainController struct {
	beego.Controller
}

func (c *MainController) Get() {
	c.Data["Website"] = "beego.me"
	c.Data["Email"] = "astaxie@gmail.com"
	c.TplName = "index.tpl"
}

给github.com/beego/beego/v2/server/web起了个别名beego,那么直接通过别名调用包中的函数即可

1.项目结构

GOROOT目录结构:

目录名说明
api每个版本的 api 变更差异
bingo 源码包编译出的编译器(go)、文档工具(godoc)、格式化工具(gofmt)
doc英文版的go文档
lib引用的一些库文件
misc杂项用途的文件,例如 Android 平台的编译、git 的提交钩子等
pkgWindows 平台编译好的中间文件
src标准库的源码
test测试用例

变量声明:

var v1 int
var v2 string
var v3 [10]int // 数组
var v4 []int // 数组切片
var v5 struct {
    f int
}
var v6 *int // 指针
var v7 map[string]int // map,key为string类型,value为int类型
var v8 func(a int) int

a := 10  // int
b := &a  // 指针
fmt.Println(a)  // 10
fmt.Println(b)  // 0xc00001a088
fmt.Println(*b)  // 10

=为给变量赋值,:=为声明变量并给变量赋值,如果使用了:=,则必须给变量赋值。

var i int
i = 10


/* 等同于 */
i := 10

匿名变量:

通过 _ 占位符来占位那些不关注的函数返回值,例如:

func GetName() (firstName, lastName, nickName string) {
    return "May", "Chan", "Chibi Maruko"
}


/* 若只想获得nickName,则函数调用语句可以用如下方式编写:*/

_, _, nickName := GetName()

数据类型:

Go语言内置以下这些基础类型:
 布尔类型:bool。
 整型:int8、byte、int16、int、uint、uintptr等。
 浮点类型:float32、float64。

 复数类型:complex64、complex128。
 字符串:string。
 字符类型:rune。
 错误类型:error。
此外,Go语言也支持以下这些复合类型:
 指针(pointer)
 数组(array)
 切片(slice)
 字典(map)
 通道(chan)
 结构体(struct)
 接口(interface)

需要注意的是,int和int32在Go语言里被认为是两种不同的类型,编译器也不会帮你自动做类型转换,比如以下的例子会有编译错误

遍历字符数组:

按字符数组方式遍历:

package main

import "fmt"

func main() {
    str := "hello,世界"
    fmt.Println(str)
    length := len(str)
    for i := 0; i < length; i++ {
        ch := str[i]
        fmt.Println(i, ch)
    }
}

以unicode字符方式遍历:

str := "Hello,世界"
for i, ch := range str {
    fmt.Println(i, ch)//ch的类型为rune
}

函数传参为值传递,所以修改变量的值并不影响原始变量。

位运算符:

安全验证 - 知乎知乎,中文互联网高质量的问答社区和创作者聚集的原创内容平台,于 2011 年 1 月正式上线,以「让人们更好的分享知识、经验和见解,找到自己的解答」为品牌使命。知乎凭借认真、专业、友善的社区氛围、独特的产品机制以及结构化和易获得的优质内容,聚集了中文互联网科技、商业、影视、时尚、文化等领域最具创造力的人群,已成为综合性、全品类、在诸多领域具有关键影响力的知识分享社区和创作者聚集的原创内容平台,建立起了以社区驱动的内容变现商业模式。https://zhuanlan.zhihu.com/p/142889698

关于数组:

数组无法扩容,分配了多少空间就可以存放多少个元素,所以产生了数组切片这个数据结构,支持动态扩容,数组切片的定义:

但是动态扩容会带来性能损失,如果开辟了五个元素的空间,现在要扩容到十个元素的空间,那么就需要开辟一块新的内存空间,然后把旧的数据拷贝过来,以此来达到扩容的目的,如此会明显降低系统整体性能,所以这是一个用空间换时间还是以时间换空间的问题。

数组切片的使用DEMO:

package main

import "fmt"

func main() {
    /* array := [5]int{1, 2, 3, 4, 5} */
    slice := make([]int, 5, 10)  /* 声明一个开辟了10个元素空间的数组切片,初始5个元素置为0 */
    slice2 := []int{7,8}  /* 声明一个两个元素的数组切片,并赋予初值7和8 */
    slice[0] = 10  /* 将该数组切片第一个元素赋值为10 */
    slice = append(slice, 1, 2, 3)  /* 数组切片添加元素 */
    slice = append(slice, slice2...)  /* 数组切片拼接另一个数组切片,通过...号来将slice2的元素一一传入 */
    for i,v := range(slice) {
        fmt.Println(i, v)
    }
}

关于map,对应的是python中的字典,java中的hashmap,DEMO:

package main
import "fmt"
// PersonInfo是一个包含个人详细信息的类型
type PersonInfo struct {
    ID string
    Name string
    Address string
}
func main() {
    var personDB map[string] PersonInfo
    personDB = make(map[string] PersonInfo)
    // 往这个map里插入几条数据
    personDB["12345"] = PersonInfo{"12345", "Tom", "Room 203,..."}
    personDB["1"] = PersonInfo{"1", "Jack", "Room 101,..."}
    // 从这个map查找键为"1234"的信息
    person, ok := personDB["1"]
    if ok {
        fmt.Println("Found person", person.Name, "with ID 1234.")
    } else {
        fmt.Println("Did not find person with ID 1234.")
    }
}

其中,personDB是声明的map变量名,string是键的类型,PersonInfo则是其中所存放的值类型。

如果要定义复杂的数据结构,例如字典的value并不是单一的数据类型,key为string,value可能为int,string,map,array等等,那么就需要使用到interface,DEMO:

package main

import "fmt"

func main() {
    a := map[string]interface{}{
        "putfh": map[string]string{"fh": "0x"},
        "int": 5,
        "string": "asddsad",
        "array": []int{1,2,3,4,5},
    }
    fmt.Println(a)
}

0X01 流程控制

1.switch case

package main

import "fmt"

func main(){
    i := 2
    switch i {
        case 0:
            fmt.Printf("0")
        case 1:
            fmt.Printf("1")
        case 2:
            fallthrough
        case 3:
            fmt.Printf("3")
    }
}

在使用switch结构时,我们需要注意以下几点:
 左花括号{必须与switch处于同一行;
 条件表达式不限制为常量或者整数;
 单个case中,可以出现多个结果选项;
 与C语言等规则相反,Go语言不需要用break来明确退出一个case;
 只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case;
 可以不设定switch之后的条件表达式,在此种情况下,整个switch结构与多个
if...else...的逻辑作用等同。

2.循环

Go中只有for循环,没有while和do-while等循环语法。

1).普通循环

package main

import "fmt"

func main() {
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }
}

2).死循环

package main

import "fmt"

func main() {
    sum := 0
    for {
        sum ++
        if sum > 50 {
            break
        } else {
            fmt.Println(sum)
        }
    }
}

3.跳转语句goto

func myfunc() {
    i := 0
    HERE:
    fmt.Println(i)
    i++
    if i < 10 {
        goto HERE
    }
}

4.panic和recover

Go 系列教程 —— 32. panic 和 recover - Go语言中文网 - Golang中文社区

1.panic

panic类似于python中的try_except,用于捕获预期之外的异常,当出现预期之外的异常后,打印堆栈信息并终止程序。DEMO:

package main

import (  
    "fmt"
)

func fullName(firstName *string, lastName *string) {  
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {  
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

函数是层级调用的,上面的程序中,首先执行main函数,在main函数中执行了fullName函数,然后在fullName中由于传递的lastName参数为nil,所以触发了panic,所以会立马打印传递给panic的错误信息和堆栈程序,然后马上终止程序,所以main函数和fullName函数中的两个fmt.Println都不会打印。

2.panic遇上defer

defer也就是延迟调用,会在函数执行完毕后调用的语句,当defer遇上了panic,调用顺序会如何?

package main

import (
    "fmt"
)

func fullName(firstName *string, lastName *string) {
    defer println("full defer 1")
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    defer println("full defer 2")
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {
    defer println("main defer 1")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
    defer println("main defer 2")
}

为什么会出现上面的结果呢?上面说过,当触发了panic后会马上终止程序,由于 main defer 2和full derfer 2的定义是在panic触发的语句之后的,所以这两个语句根本没有执行。而main defer 1和full defer 1是在panic触发之前定义的,所以这两个语句可以正常执行。并且当panic遇上了defer,会先调用已经正常定义的defer语句,然后打印panic信息和堆栈。

3.recover

recover是一个内建函数,用于重新获得 panic 协程的控制。只有在延迟函数的内部,调用 recover 才有用。在延迟函数内调用 recover,可以取到 panic 的错误信息,并且停止 panic 续发事件(Panicking Sequence),程序运行恢复正常。如果在延迟函数的外部调用 recover,就不能停止 panic 续发事件。我们来修改一下程序,在发生 panic 之后,使用 recover 来恢复正常的运行。

就相当于try_except嘛,捕获到了异常之后可继续执行程序。

0X02 函数

func fa(a int, b int) (c int, err error) {
    return a+b, nil
}

1.闭包

闭包是匿名函数与匿名函数所引用环境的组合。匿名函数有动态创建的特性,该特性使得匿名函数不用通过参数传递的方式,就可以直接引用外部的变量。这就类似于常规函数直接使用全局变量一样,个人理解为:匿名函数和它引用的变量以及环境,类似常规函数引用全局变量处于一个包的环境。

package main
import (
    "fmt"
)
func main() {
    var j int = 5
    a := func()(func()) {
        var i int = 10
        return func() {
            fmt.Printf("i, j: %d, %d\n", i, j)
        } 
    }()
    a()
    j *= 2
    a()
}

类似python中的装饰器,返回函数的函数,在外部匿名函数中返回了另一个函数,就也是执行Printf的函数,所以首次调用时a等于一个闭包,这个闭包包含了Printf这个函数和变量i,那么调用a这个闭包时会输出i和j的值,也就是10,5。然后将j*2后,将会输出10,10。由于变量i是闭包内部的变量,所以无法在外部修改,就算在外部修改了,也会打印闭包内部变量i的值。

2.defer

在函数返回前调用的语句或函数,经常用来关闭文件、关闭连接等操作,防止因为异常导致链接未释放的问题。

func CopyFile(dst, src string) (w int64, err error) {
    srcFile, err := os.Open(src)
    if err != nil {
        return
    }
    defer srcFile.Close()

    dstFile, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dstFile.Close()
    return io.Copy(dstFile, srcFile)
}

0X03 面向对象编程

Go中没有类的概念,也没有class关键字,只有结构体,但是结构体相当于其他语言的类了,总而言之都是可以面向对象编程的,实现方式不同罢了。并且也没有继承,构造函数,析构函数这种概念,但是通过组合这个概念也可以达到继承的效果。

1.类型系统

2.函数和方法

函数的定义:

func func_name(params params_type)(return return_type) {
    // body
}

方法:

由于在Go中没有类的定义,但是允许自定义结构体或者自定义类型,所以使用方法来给自定义的结构体或者类型添加绑定的函数。所以需要在函数名前指定绑定该方法的对象,例如:

type Rect struct {
    x, y float64
    width, height float64
}

func (r *Rect) Area() float64 {
    return r.width * r.height
}

3.值传递和引用传递

(1)Go在值传递上践行得非常彻底,基本全部是值传递,例如数组:

var a = [3]int{1,2,3}
var b = a
b[1] = 3
fmt.Println(a, b)

// [1,2,3]
// [1,3,3]

说明b是完全复制了一份a,占用另一块内存地址,如果要想引用,则需要使用到指针:

var a = [3]int{1,2,3}
var b = &a
b[1] = 3
fmt.Println(a, *b)

// [1,3,3]
// [1,3,3]

&为取地址符号,*为访问该地址指向的对象

(2)值类型和引用类型

上面说到,Go基本上都是值传递,这点与Python类似,但是仍然存在例外,有使用引用传递的数据类型。

值传递的数据类型int,float,bool,string,数组,结构体(struct)
引用传递的数据类型map,slice,channel,interface

对于值类型,作为参数传参时就是将值复制了一份。对于引用类型,其本身存放的就是一个内存地址,所以无需再使用指针符号。

4.自定义结构体的初始化

没有显式初始化的值都会被赋予一个默认值,例如int类型为0,bool为false, string类型为空串。

rect1 := new(Rect)
rect2 := &Rect{}
rect3 := &Rect{0, 0, 1, 1}
rect4 := &Rect{Width: 100, Height: 100}

在Go中没有构造函数的概念,对象的创建通常由一个全局的创建函数完成,以NewXXX命名,用来表示构造函数,例如:

func NewRect(x, y, width, height float64) *Rect {
    return &Rect{x, y, width, height}
}

5.匿名组合和具名组合golang 匿名结构体成员,具名结构体成员,继承,组合_翔云-CSDN博客

(1)匿名组合

匿名组合可以达到继承的效果,说白了就是对一个结构体进行二次封装,可以添加一些属性和方法。

匿名组合的定义,可以看到在Y结构体中包含了X结构体:

注意:在Y中是直接包含的X结构体,而不是X结构体实例化的对象,这样称为匿名组合,Y结构体会继承X结构体的所有属性和方法

type X struct {
    Name string
}

type Y struct {
    X
    Name string
}

那么Y的初始化方式有两种,一种是在初始化Y的时候顺便一起初始化X,另一种是先初始X然后再初始化Y。此处代码没有BUG,但是逻辑上似乎有点奇怪,待续

一起初始化:

package main

import "fmt"

type X struct {
    Name string
}

type Y struct {
    X
    Name string
}

func main() {
    y := &Y{X{"this is Y_X"}, "this is y"}
    fmt.Println(y.Name)
    fmt.Println(y.X.Name)
}

先初始化X,在初始化Y:

package main

import "fmt"

type X struct {
    Name string
}

type Y struct {
    X
    Name string
}

func main() {
    x := X{"this is X"}
    y := &Y{x, "this is y"}
    fmt.Println(y.Name)
    fmt.Println(y.X.Name)
}

那么这里就有个思考,可以看到Y结构体包含的X结构体,并不是指针对象,那么就是值传递,那么先声明了x,在声明了y,通过修改y.X.Name的值应该不会影响到变量x的值:(确实如此)

package main

import "fmt"

type X struct {
    Name string
}

type Y struct {
    X
    Name string
}

func main() {
    x := X{"this is X"}
    y := &Y{x, "this is y"}
    fmt.Println(y.Name)
    fmt.Println(y.X.Name)
    y.X.Name = "modified X"
    fmt.Println(y.X.Name)
    fmt.Println(x.Name)
}

(2)具名组合

type X struct {
    Name string
}

type Y struct {
    x X
    Name string
}

6.接口Interface

接口目前看来主要有三大功能,一个协商好对外暴露的调用方式,例如规定动物接口需要有move,eat方法,利于合作开发。二是可以在最外层调用时使用接口对象实例化具体的实现结构体,相当于门面模式。三是可以将接口类型作为参数传递给函数或者方法,那么实现了该接口的对象都能作为参数传递给这个函数或者方法:

package main
import (
    "fmt"
)
//Shaper接口
type Shaper interface {
    Area() float64
}
// Circle struct结构体
type Circle struct {
    radius float64
}
// Circle类型实现Shaper中的方法Area()
func (c *Circle) Area() float64 {
    return 3.14 * c.radius * c.radius
}

func myArea(n Shaper) {
    fmt.Println(n.Area())
}

func main() {
    // Circle的指针类型实例
    c1 := new(Circle)
    c1.radius = 2.5
    //将 Circle的指针类型实例c1传给函数myArea,接收类型为Shaper接口
    myArea(c1)
}

接口赋值:

1.将对象实例赋值给接口:

要求该对象实现了接口定义的所有方法,才能赋值

Go可以根据"类型"的函数,自动生成"类型指针"的函数.但是无法根据"类型指针"的函数生成"类型"的函数。例如针对以下struct和interface:

package main

import "fmt"

// 定义一个新类型Integer,继承于int
type Integer int

// 定义Integer的add方法
func (a Integer) check (b Integer) bool {
    return a < b
}

// 定义Integer的increase方法
func (a *Integer) increase (b Integer) {
    *a += b
}

// 定义接口,需要注意的是,接口函数中的参数是值传递,没有使用指针
type LessAddress interface {
    check(b Integer) bool
    increase(b Integer)
}

func main() {
    var a Integer = 1
    // var less_add LessAddress = a 该赋值会报错
    var less_add LessAddress = &a
    fmt.Println(less_add.check(2))
}

针对以指针为参数的函数,目的是想操作该指针指向的对象,如果根据定义的指针参数函数去生成值参数函数那么函数意义就完全不同了,这样大概率不符合用户的预期,所以没有自动根据指针参数生成值参数的功能。

2.将一个接口赋值给另一个接口

接口查询:可以查询一个接口指向的对象是否实现了某个接口。

类型查询:可以查询一个接口指向的对象的类型

接口组合:将多个接口组合起来

空接口:interface{} 什么函数都没定义的接口,所以任何对象都可以赋值给该接口,当函数可以接受任何对象实例时就可以用空接口来占位

0X04 并发编程

关于Go的并发&并行:

首先,我们得区分什么是并发什么是并行,举个比较熟悉的例子,并发就是一个锅同时炒2个菜,2个菜来回切换,并行就是你有多个锅,每个锅炒不同的菜,多个锅同时炒!

对于计算机来说,这个锅就是CPU,单核CPU同一时间只能执行一个程序,但是CPU却可以在不同程序之间快速切换,所以你在浏览网页的同时还可以听歌!但是多核CPU就不一样了,操作系统可以一个CPU核心用来浏览网页,另一个CPU核心拿来听歌,所以多核CPU还是有用的。

但是对于单一程序来说,基本上是很难利用多核CPU的,主要是编程实现非常麻烦,这也是为什么很多人都说多核CPU是一核有难多核围观...特别是一些比较老的程序,人家在设计的时候压根没考虑到多核CPU,毕竟10年前CPU还没有这么多核心。

单核CPU只能并发,不能并行,毕竟只有一个核心,每个时刻只有一个线程可以获取CPU的执行权限。

(关于线程的轮转,CPU有时间片机制,每个线程执行多少时间后自动进行切换,而Go协程是遇到io阻塞主动让出CPU给别的协程,无需等待CPU下一次轮转,所以性能会更高)

1.几种并发实现方式

2.协程

Golang协程详解和应用 - 知乎

协程为轻量级线程,最大的优势就在于轻量。与传统系统级的进程和线程相比,系统往往创建的进程和线程最多不超过1万个,而作为用户态的线程--协程,系统可以轻松创建上百万个而不会导致资源衰竭。

多数语言在语法层面并不直接支持协程,而是通过库的方式支持,但用库的方式支持的功能也并不完整,比如仅仅提供轻量级线程的创建、销毁与切换等能力。如果在这样的轻量级线程中调用一个同步 IO 操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行轻量级线程,从而无法真正达到轻量级线程本身期望达到的目标。
Go 语言在语言级别支持轻量级线程,叫goroutine。Go 语言标准库提供的所有系统调用操作(当然也包括所有同步 IO 操作),都会出让 CPU 给其他goroutine。这让事情变得非常简单,让轻量级线程的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量。

来个最简单的代码例子,通过go关键字创建协程:

package main

import "fmt"

func add(a, b int) {
    c := a + b
    fmt.Println(c)
}

func main() {
    for i := 0; i < 10; i++ {
        go add(i, i)
    }
}

运行这段代码会没有任何输出,因为主函数循环执行的速度大于协程启动的速度,所以主函数循环执行完毕然后退出了,10个协程可能都没有启动完,自然没有输出。

3.并发通信

(1)基本概念

并发场景下通信是一个大话题,也就是多个并发单元之间如何相互通信,通常有两种模式:共享数据消息

共享数据:共享数据是指多个并发单元分别保持对同一个数据的引用,实现对该数据的共享。被共享的数据可能有多种形式,比如内存数据块、磁盘文件、网络数据等。在实际工程应用中最常见的无疑是内存了,也就是常说的共享内存。由于多个并发单元操作同一个数据,所以通常都会加入锁机制,那么代码就会很冗余,到处都是对锁的申请和释放。

消息:channel是Go在语言级别提供的goroutine间的通信方式,可以通过channel在两个或者多个goroutine之间传递消息。channel是进程内的通信方式,如果需要跨进程通信推荐使用Socket或者HTTP等协议。

Go的并发通信哲学:不要通过共享内存来通信,而应该通过通信来共享内存

Channel语法:

var chanName chan ElementType

与一般的变量声明不同的地方仅仅是在类型之前加了chan关键字。ElementType指定这个channel所能传递的元素类型,比如说我们定义一个传递int类型的channel:

var ch chan int

定义一个channel也很容易,使用make关键字即可(无缓冲区):

ch := make(chan int)

将数据写入channel:

ch <- 1

注意:向channel写入数据会导致程序阻塞,直到有其他goroutine从这个channel读取数据!

但是协程的阻塞并不会影响主线程的执行,除非在主线程中设置了等待其他协程执行完毕,例如以下代码,协程的阻塞对主线程并无影响:

package main

import "fmt"
import "time"


func main() {
    c := make(chan int)
    go func() {
        fmt.Println("Write 5 to channel")
        c <- 5
        fmt.Println("finish")
    }()
    time.Sleep(5 * time.Second)
}

select关键字:

早在Unix时代,select机制就已经被引入。通过调用select()函数来监控一系列的文件句柄,一旦其中一个文件句柄发生了IO动作,该select()调用就会被返回。后来该机制也被用于实现高并发的Socket服务器程序。Go语言直接在语言级别支持select关键字,用于处理异步IO问题

select语句类似switch case,最大的不同是select语句的case块后面必须是一个IO操作:

select {
    case <-chan1:
    // 如果chan1成功读到数据,则进行该case处理语句
    case chan2 <- 1:
    // 如果成功向chan2写入数据,则进行该case处理语句
    default:
    // 如果上面都没有成功,则进入default处理流程
}

带缓冲区的channel:

在声明channel时携带第二个参数即可:

c := make(chan int, 1024)

缓冲区的意义在于,及时没有读取方,写入方也可以一直写入,在缓冲区被填满之前都不会阻塞。

(2)异常处理

当从一个channel读取数据,但这个channel为空且一直没有程序往该channel写入数据的话,就会导致goroutine永远阻塞,这时我们需要引入超时机制:

// 首先,我们实现并执行一个匿名的超时等待函数
timeout := make(chan bool, 1)
go func() {
    time.Sleep(1e9) // 等待1秒钟
    timeout <- true
}()

// 然后我们把timeout这个channel利用起来
select {
    case <-ch:
    // 从ch中读取到数据
    case <-timeout:
    // 一直没有从ch中读取到数据,但从timeout中读取到了数据
}

0X05 工程管理

我认为工程管理是在开发过程中十分重要的一环,先前看过一些言论,说只用几天或者一周时间就能学会一门语言,有一说一,我觉得挺扯淡的。如果说他们所谓的学会指的是能写代码,但是不管代码质量,不管工程化,更不管运维,那确实一周时间可能足够了。但是优秀的代码一定是语言优雅,结构清晰,可维护性强的,这就需要开发者对语言足够了解,有丰富的开发经验,这样才能写出优雅易维护的项目。

1.Go命令行工具

安装Go后自带命令行工具,例如go version,go build,go run等,go help可以查看所有的命令行指令。

2.代码风格

代码必须是本着写给人阅读的原则来编写,只不过顺便给机器执行而已”。Go作为极度工程化的语言,做了很多强制的编码规范,如果未按照规范编码会导致编译错误,可以说是极致的要求了。

3.强制性编码规范

(1)命名规范

任何需要对外暴露的名字必须首字母大写,不需要对外暴露的名字必须首字母小写。

Go强制采用驼峰命名法,而不是下划线命名法。

(2)花括号要求

Go对花括号的位置也有着强制要求,例如if else语句,if语句的反花括号必须和else同一行,如果出现了别的格式,那么编译器会直接报错。正确写法:

func Foo(a, b int)(ret int, err error) { 
    if a > b { 
        ret = a 
    } else { 
        ret = b 
    } 
}

4.非强制性编码建议

go命令行工具提供了fmt命令,可以对go代码进行结构美化,转换成官方推荐的格式。

go fmt hello.go

该工具不仅可以美化单个文件,也可以美化一个目录下的所有go文件。需要值得注意的是,我们并不该依赖工具帮我们进行代码美化,而应该养成良好的编码习惯,直接写出优雅的代码。

5.远程import

go允许直接import远程包,例如github上的g远程包,然后在编译代码执行执行go get获取该包即可。

6.工程组织

Go项目结构:Go 学习笔记1——【项目结构】_zd398900463的博客-CSDN博客_go项目结构

关于go build和go install:深入理解 go build 和 go install - 简书
(1)go build 生成可执行文件在当前目录下, 当main package存在时,go install会将库文件编译后存放在pkg目录下,并且生成可执行文件在bin目录下($GOPATH/bin)。当maik package不存在时,go install仅仅会将库文件编译后存放在pkg目录下。使用Goland作为IDE的话,如果只配置了Global GOPATH,那么可执行文件保存在GOPATH/bin目录下。如果设置了Project GOPATH,则会将可执行文件保存在Project GOPATH/bin目录下。

(2)go build 经常用于编译测试。go install主要用于生产库和工具

(3)go build只会生成单独的一个可执行文件,而go install还会生成相关的库文件和可执行文件并保存到固定目录下,例如:

  • go install编译出的可执行文件以其所在目录名(DIR)命名
  • go install将可执行文件安装到与src同级别的bin目录下,bin目录由go install自动创建
  • go install将可执行文件依赖的各种package编译后,放在与src同级别的pkg目录下
  • 如果在GOPATH以外的目录执行go install会报错:
  • 当某个库曾经编译过时,不会重复编译。可以添加-a参数进行强制重新编译

(4)src 目录
用于以包(package)的形式组织并存放 Go 源文件,这里的包与 src 下的每个子目录是一一对应。例如,若一个源文件被声明属于 log 包,那么它就应当保存在 src/log 目录中。

并不是说 src 目录下不能存放 Go 源文件,一般在测试或演示的时候也可以把 Go 源文件直接放在 src 目录下,但是这么做的话就只能声明该源文件属于 main 包了。正常开发中还是建议大家把 Go 源文件放入特定的目录中。

包是Go语言管理代码的重要机制,其作用类似于Java中的 package 和 C/C++ 的头文件。Go 源文件中第一段有效代码必须是package <包名> 的形式,如 package hello。

另外需要注意的是,Go语言会把通过go get 命令获取到的库源文件下载到 src 目录下对应的文件夹当中。

(5)pkg 目录
用于存放通过go install 命令安装某个包后的归档文件。归档文件是指那些名称以“.a”结尾的文件。

该目录与 GOROOT 目录(也就是Go语言的安装目录)下的 pkg 目录功能类似,区别在于这里的 pkg 目录专门用来存放项目代码的归档文件。

编译和安装项目代码的过程一般会以代码包为单位进行,比如 log 包被编译安装后,将生成一个名为 log.a 的归档文件,并存放在当前项目的 pkg 目录下。

pkg/mod/存储的是第三方库源代码,通过go.mod下载的第三方库存储在该处,不存在src目录下。

(6)bin 目录
与 pkg 目录类似,在通过go install 命令完成安装后,保存由 Go 命令源文件生成的可执行文件。在类 Unix 操作系统下,这个可执行文件的名称与命令源文件的文件名相同。而在 Windows 操作系统下,这个可执行文件的名称则是命令源文件的文件名加 .exe 后缀。
(7)关于Go导包(Golang Package 与 Module 简介 - 简书)

  • Go使用包来管理代码,一个包含go文件的目录就是一个包,在需要导入库时都是以包为单位,而不是以go文件为单位
  • import语句导入的是包的路径,例如有个包的目录为go-test,package名为test,那么通过import导入时应该写 "xxx/xxxx/go-test",但是在调用该包的函数时,需要用package名调用,即test.xxxxx。
  • go.mod中会定义模块名,引入该模块的package就需要通过该模块名去引入
  • 处于同一个目录下的go文件,package名需要统一,不能定义为不同的名字,但是如果是嵌套目录,package名可以不一致,例如:

                                        

 dialer目录下的go文件package名为dialer,elastic目录下的go文件package名为elastic,而这两个目录的父目录helper下的go文件package名为helper。

  • 在没有go.mod时,下载的第三方库和用户自己的项目代码都需要放在GOPATH/src目录下,当导入标准库时,Go会从GOROOT中查找,当导入第三方库时Go会从GOPATH/src中查找,所以GOPATH/src下需要保存第三方库的源码且和用户项目代码混杂在一起,非常不利于管理。
  • 在引入了go.mod后,第三方库统一存储在GOPATH/pkg/mod目录下,且用户项目代码不再需要存储到GOPATH/src目录下了,只需要在go.mod中定义需要导入的第三方库即可,Go会帮用户下载所需的第三方库并导入。
  • 在使用go.mod后,如果要引入当前module的其他package,需要带上module名,例如:

在metricbeat的config.go文件中需要引入libbeat的代码,不能直接import libbeat/autodiscover,需要加上module的前缀,module名在go.mod中会定义,例如:

(8)标准的Go项目目录结构

(9)文档管理

Go支持通过go doc命令直接生成代码文档,go doc会提取go文件中的包信息、函数信息及相关注释。

go doc foo  /* 输出相关内容到控制台 */
godoc -http=:76 -path="."  /* 可通过浏览器访问相关内容,更清晰 */

如果需要写在代码中的注释能够被go doc正确识别,需要满足以下要求:

  • 注释需要紧贴在对应的包声明和函数声明前,不能有空行
  • 如果需要记录BUG,可以使用 // BUG(author): 的格式记录遗留问题,这些遗留问题也会显示在go doc中
  • 注释如果要新起一个段落,应该用一个空白注释行隔开,因为直接换行书写会被认为是正常的段内折行
  • 更多便捷的功能自行使用 go doc创建的http server进行体验

7.单元测试

执行功能单元测试非常简单,直接执行go test命令即可,下面的代码用于对整个simplemath 包进行单元测试:

关键点记录:

A Tour of GO:https://tour.golang.org/list

1.模块中需要导出的函数,首字母必须大写,例如在calc中导入add.go中的Add函数,Add函数名首字母必须大写。小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。这个规则也适用于类型和变量的可见性。

2.同Go语言的其他符号(symbol)一样,以大写字母开头的常量在包外可见。

在此记录一个大坑,关于Go把结构体写入到文件中,结构体名和结构体内字段名必须首字母大写,否则写入到文件的会是空结构,只有{},并且全程没有任何报错。幸好在一篇博客中看到了相关描述,不然真的要被坑死

链接:Go的50个坑:Go的50坑:新Golang开发者要注意的陷阱、技巧和常见错误[1] - Mr.毛小毛 - 博客园

英文原文:50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs

3.关于Go协程和Python协程的对比:(https://www.programmersought.com/article/72171418609/

go的协程本质上还是系统的线程调用,而Python中的协程是eventloop模型实现,所以虽然都叫协程,但并不是一个东西.
Python 中的协程是严格的 1:N 关系,也就是一个线程对应了多个协程。虽然可以实现异步I/O,但是不能有效利用多核(GIL)。
而 Go 中是 M:N 的关系,也就是 N 个协程会映射分配到 M 个线程上,这样带来了两点好处:

  • 多个线程能分配到不同核心上,CPU 密集的应用使用 goroutine 也会获得加速.
  • 即使有少量阻塞的操作,也只会阻塞某个 worker 线程,而不会把整个程序阻塞。

4.package名和目录名称最好保持一致

5.Go从1.13开始支持Go module了,用来取代Go path作为项目的工作空间,Go Module用来存放自己的项目文件,Go path用来存放网络上下载的第三方库,当我们自己的项目需要依赖第三方的包的时候,我们通过GoModule目录下的一个go.mod文件来引用GoPath目录src包下的第三方依赖即可:Golang中的GoPath和GoModule_千里之行,始于足下-CSDN博客

使用go get命令可以下载第三方库到Go path中,然后在go.mod中添加对该库的引用即可,例如:

require github.com/beego/beego/v2 v2.0.0 // indirect

6.Go语言默认函数传参为值传递,如果需要改变原有变量的值,那么需要使用指针

7.关于指针

    1)*操作符作为右值时,意义是取指针的值,作为左值时,也就是放在赋值操作符的左边时,表示 a 指针指向的变量。其实归纳起来,*操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值,当操作在左值时,就是将值设置给指向的变量。

*p = 10  // 将p指针指向10
b = *p  // 将p指针指向的对象的值赋值给b

    2)指针变量也是有类型的,和指针指向的对象类型有关,例如指向了string,那么就是指向string类型的指针。指向了int,那么就是指向了int类型的指针

    3)指针被定义后如果没有指向任何对象,那么指针的值为nil

    4)关于访问结构体字段:

Struct fields can be accessed through a struct pointer.
结构体字段可以使用结构体指针获取。

To access the field X of a struct when we have the struct pointer p we could write (*p).X. However, that notation is cumbersome, so the language permits us instead to write just p.X, without the explicit dereference.
结构体指针访问字段本来应该写成(*p).x,但是由于这么写太蠢了,所以允许直接写成p.x。

8.GOROOT为Go的安装路径,GOPATH为存储全局第三方库的路径

9.Go 在最初刚刚发布的时候,静态链接被当做优点宣传,只须编译后的一个可执行文件,无须附加任何东西就能部署。将运行时、依赖库直接打包到可执行文件内部,简化了部署和发布的操作,无须事先安装运行环境和下载诸多第三方库。不过最新版本却又加入了动态链接的内容了。

10.打印变量类型:

import reflect

fmt.Println(reflect.TypeOf(fd))

11.Go中数组的元素个数必须是一个确定的值,例如1,2,3,4,5,不能是变量的值。如果需要使用变量的值作为数组元素个数,则需要使用数组切片。

12.Go语言的字符有以下两种:

  • 一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
  • 另一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。

13.数组和数组切片是两种不同的数据格式,所以作为参数或者返回值的话,两者不可视为一种

14.逻辑运算,并且:&&,或者:||

15.关于数组切片:声明一个数组切片并且元素个数不为0的话,Go会给元素设置默认值,除非将该下标的元素覆盖,否则该元素会一直存在。如果是使用append的方法为切片添加元素,那么可以将切片的元素个数初始化为0个,然后慢慢添加元素。

16.包名和文件夹名的区别:

假设文件夹名称为:student_manage,  包名为:student
那么在main中使用时,应该import student_manage ,但真实调用其方法时应该是student.QueryStudent("stuNo"),在真实项目中,go官方建议文件夹名称和包的名称一样,以防止歧义产生,同时尽量简短。

17.如果修改了go.mod中的module名:

如果您更改文件中的module名称go.mod,则必须import用更新后的module名称替换所有路径。当您更换你的模块github.com/foo/bar/v3github.com/foo/bar/v4,你必须find and replace的所有参考github.com/foo/bar/v3github.com/foo/bar/v4贯穿整个项目。那么你$ go test -v应该运行正常。

18.结构体字段注释DEMO:

type Process struct {
        PID   string   `json:"pid,omitempty"   yaml:"pid,omitempty"`
        PPID  string   `json:"ppid,omitempty"  yaml:"ppid,omitempty"`
        Title string   `json:"title,omitempty" yaml:"title,omitempty"`
        Name  string   `json:"name,omitempty"  yaml:"name,omitempty"` // Comm
        Exe   string   `json:"exe,omitempty"   yaml:"exe,omitempty"`
        CWD   string   `json:"cwd,omitempty"   yaml:"cwd,omitempty"`
        Args  []string `json:"args,omitempty"  yaml:"args,omitempty"`
}

19.byte和rune,单引号双引号和反引号

  • byte实际就是uint8类型,代表了ASCII码的一个字符。
  • rune 实际就是int32类型,代表一个 UTF-8 字符,为该字符的unicode码值。当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。

单引号:使用rune类型来表示字符

双引号:创建支持转义的字符串,

反引号:反引号用来创建不支持转义的原生字符串,类似Python的 r" "

索引字符访问的是单个字节而不是字符,相比之下, range 循环在每次迭代中会解码一个 UTF-8 编码 rune。每次循环时,循环的索引都是当前 rune 的起始位置 (以字节为单位),码点是其值。

字符串是由字节构建的,因此对它们进行索引将生成字节,而不是字符。字符串甚至可能不包含字符。实际上,“字符” 的定义是模棱两可的,试图通过定义字符串是由字符组成这种说法来解决歧义是错误的

20.Goland实用设置:GoLand实用技巧 - 知乎

21.go env配置

# 开启go module
go env -w GO111MODULE=on


# 可以通过GOPROXY来控制代理, 通过 GOPRIVATE 控制私有库不走代理, 设置GOPROXY代理:

go env -w GOPROXY=https://goproxy.cn,direct

# 设置GOPRIVATE来跳过私有库,比如常用的Gitlab或Gitee,中间使用逗号分隔:

go env -w GOPRIVATE=*.gitlab.com,*.gitee.com

# 如果在运行go mod vendor时,提示Get https://sum.golang.org/lookup/xxxxxx: dial tcp 216.58.200.49:443: i/o timeout,则是因为Go 1.13设置了默认的GOSUMDB=sum.golang.org,这个网站是被墙了的,用于验证包的有效性,可以通过如下命令关闭:

go env -w GOSUMDB=off

# 私有仓库自动忽略验证

可以设置 GOSUMDB="sum.golang.google.cn", 这个是专门为国内提供的sum 验证服务。

-w 标记 要求一个或多个形式为 NAME=VALUE 的参数, 并且覆盖默认的设置

# 在/etc/profile中设置了go env的变量话,不能用go env -w 去覆盖,那么就需要修改/etc/profile
export GOROOT=/usr/local/go
export GOPATH=/home/go/GOPATH
export PATH=$PATH:$GOROOT/bin:$GOROOT/bin:$GOPATH/bin:$GOPATH/bin:
export GOPROXY="https://goproxy.cn",direct

22.在使用Goland时,有时候下载了第三方库但是在Goland中第三方库还是被标红,虽然并不影响程序运行,但是会无法使用IDE的提示功能。造成这样的原因是本地存在多个版本的第三方模块缓存, 解决方法:可以执行go clean --modcache。再执行go run main.go或者go build 重新编译
或者点击Goland标红处,点击sync dependency即可

23.golang中的协程不存在嵌套关系,即如果在协程中再运行一个协程,那么这两个协程是没有父子关系的,都是独立的协程,协程只和main进程有依赖关系,当main进程退出时,所有协程都会退出

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
学习 Go(也称为 Golang)语言从入门到精通可以分为以下几个步骤: 1. **安装和环境设置**: - 官方下载 Go 的安装包并配置好 GOPATH(Go 工作路径),用于存放源码、依赖等。 2. **基础语法**: - 学习基本的数据类型(如 int, float, string 等)、变量声明、常量定义。 - 掌握流程控制结构(if-else, for, while, switch)和函数的定义与调用。 3. **模块管理**: - 使用 `go mod` 命令来管理和导入外部库(模块),了解如何编写和使用 `import` 和 `package` 关键字。 4. **并发编程**: - Go 强调并发,理解 Goroutines(轻量级线程)和 Channels(管道通信机制)的概念。 - 学习互斥锁(sync.Mutex)和通道选择器(select)等同步原语。 5. **标准库的探索**: - 熟悉标准库提供的常用功能,如 fmt (格式化)、io (输入/输出)、net (网络)、os (操作系统接口) 等。 6. **HTTP服务器与客户端**: - 学会使用 net/http 包创建简单的 HTTP 服务端和客户端。 7. **Web框架**: - 如果对 Web 开发感兴趣,可以尝试 Gin 或 Beego 这样的轻量级框架。 8. **错误处理与日志记录**: - 学习如何优雅地处理和捕获运行时错误,以及使用 logrus 或 zap 进行日志记录。 9. **项目实战**: - 通过实际项目练习,比如搭建简单的 RESTful API、数据处理工具或游戏后端。 10. **进阶主题**: - 对于高级开发者,可研究 goroutine 性能优化、内存管理(垃圾回收机制)、反射、接口和组合等概念。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值