0%

GO语法笔记

go语法笔记

  • hello world

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package main

    import "fmt"

    func main() {
    var firstname string
    fmt.Scanln(&firstName)
    fmt.Println(firstName)
    }
    //Println的p需要大写
    //命令行运行go run 文件名

println is an built-in function (into the runtime) which may eventually be removed, while the fmt package is in the standard library, which will persist. See the spec on that topic.

For language developers it is handy to have a println without dependencies, but the way to go is to use the fmt package or something similar (log for example).

变量

  • 定义变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var firstName string
    //或者直接不指定变量类型直接赋值,go会自己推断
    var (
    firstName,lastName = "harry","smith"
    )

    //使用:=赋值,要声明的变量必须是新变量。 如果使用冒号等于号并已经声明该变量,将不会对程序进行编译。
    func main() {
    firstName, lastName := "John", "Doe"
    fmt.Println(firstName, lastName)
    }

  • 定义常量

    1
    2
    3
    4
    5
    6
    7
    const httpstatus = 200
    const (
    StatusOK = 0
    StatusConnectionReset = 1
    StatusOtherError = 2
    )
    //不能使用冒号等于号来声明常量

如果变量定义了未使用go会抛出错误

数据类型

Go 有四类数据类型:

  • 基本类型:数字、字符串和布尔值
  • 聚合类型:数组和结构
  • 引用类型:指针、切片、映射、函数和通道
  • 接口类型:接口

数字

一般使用 int (当计算机是32位时表示32位的int , 64位时位64位int)

Go 还提供了 int8int16int32int64 类型,其大小分别为 8、16、32 或 64 位的整数

只有float32 float64两种

布耳

1
2
3
// go的bool类型只能用于显式赋值,0,1不能标识对应的布耳值
var flag bool = true

字符串

string

1
2
fullName := "John Doe \t(alias \"Foo\")\n"
fmt.Println(fullName)
  • 转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //使用自带的函数转换
    var integer16 int16 = 127
    var integer32 int32 = 32767
    fmt.Println(int32(integer16) + integer32)

    //使用strconv包来转换
    import "strconv"

    func main() {
    i, _ := strconv.Atoi("-42")
    s := strconv.Itoa(-42)
    fmt.println(i, s)
    }
    //下划线 (_) 用作变量的名称(如果他是变量程序没法编译,因为变量需要用到使用才能通过编译)。 在 Go 中,这意味着我们不会使用该变量的值,而是要将其忽略。

变量默认值

  • int 类型的 0(及其所有子类型,如 int64
  • float32float64 类型的 +0.000000e+000
  • bool 类型的 false
  • string 类型的空值

数组

要在 Go 中声明数组,必须定义其元素的数据类型以及该数组可容纳的元素数目。 然后,可采用下标表示法访问数组中的每个元素,其中第一个元素是 0,最后一个元素是数组长度减去 1(长度 - 1)。

1
2
3
4
5
6
7
8
var a [3]int
a[1] = 10

//给数组初始值
cities := [5]string{"New York", "Paris", "Berlin", "Madrid"}
fmt.Println("Cities:", cities)
//支持多维数组是
var twoD [3][5]int

slice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
quarter1 := months[0:3]
quarter2 := months[3:6]
quarter3 := months[6:9]
quarter4 := months[9:12]
//len函数为长度,cap为容量会自动向后拓展直到整个数组
fmt.Println(quarter1, len(quarter1), cap(quarter1))
fmt.Println(quarter2, len(quarter2), cap(quarter2))
fmt.Println(quarter3, len(quarter3), cap(quarter3))
fmt.Println(quarter4, len(quarter4), cap(quarter4))

//output
[January February March] 3 12
[April May June] 3 9
[July August September] 3 6
[October November December] 3 3

  • append(原数组,添加元素)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//拓展切片的capacity 函数append()
var numbers []int
for i := 0; i < 10; i++ {
numbers = append(numbers, i)
fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)
}
//输出
0 cap=1 [0]
1 cap=2 [0 1]
2 cap=4 [0 1 2]
3 cap=4 [0 1 2 3]
4 cap=8 [0 1 2 3 4]
5 cap=8 [0 1 2 3 4 5]
6 cap=8 [0 1 2 3 4 5 6]
7 cap=8 [0 1 2 3 4 5 6 7]
8 cap=16 [0 1 2 3 4 5 6 7 8]
9 cap=16 [0 1 2 3 4 5 6 7 8 9]
/*
当切片容量不足以容纳更多元素时,Go 的容量将翻倍。 它将新建一个具有新容量的基础数组。 无需执行任何操作即可使容量增加。 Go 会自动扩充容量。 需要谨慎操作。 有时,一个切片具有的容量可能比它需要的多得多,这样你将会浪费内存。*/
  • 删除切片中的元素,只能用append()来实现

    1
    2
    3
    4
    a := []int{1, 2, 3, 4, 5, 6}
    a = append(a[:2], a[3:]...)
    //删除了元素值'2'的元素
    //[1 2 4 5 6]
  • 创建副本 

    Go 具有内置函数 copy(dst, src []Type) 用于创建切片的副本。 你需要发送目标切片和源切片。

1
2
3
4
5
letters := []string{"A", "B", "C", "D", "E"}
slice2 := make([]string, 3)
copy(slice2, letters[1:4])
fmt.Println(slice2)
// [B C D]

map(key-value)

1
2
3
4
5
6
  studentsAge := map[string]int{
"john": 32,
"bob": 31,
}
fmt.Println(studentsAge)
//map[bob:31 john:32]

map操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

//创建一个空的map
studentsAge := make(map[string]int)

//创建新项
studentsAge["john"] = 32

//判断是否存在map中
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31

age, exist := studentsAge["christy"]
if exist {
fmt.Println("Christy's age is", age)
} else {
fmt.Println("Christy's age couldn't be found")
}


//删除项
delete(studentAge,"john")
//删除不存在的项,你不会遇到错误,而且会看到以下输出:map[bob:31 john:32]
delete(studentsAge, "christy")


//遍历map
for name, age := range studentsAge {
fmt.Printf("%s\t%d\n", name, age)
}

struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

//格式,类似c语言的结构体
type Person struct {
ID int
FirstName string
LastName string
Address string
}


//可以理解成 继承,或者嵌套,Employee中嵌套了Person的结构体
type Employee struct {
Person
ManagerID int
}

type Contractor struct {
Person
CompanyID int
}

func main() {
employee := Employee{}
employee.LastName = "Doe"
fmt.Println(employee.LastName)
}
//Doe

json序列化及其反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
"encoding/json"
"fmt"
)

type Person struct {
ID int
/*这里的反引号部分是struct tag,可以决定序列化的结果
比如这里FirstName写了name
当序列化成json的时候key值就会变成name而不是FirstName
*/
FirstName string `json:"name"`
LastName string
//这里的omitempty含义是省略,当address为value空时,省略这个键值对
Address string `json:"address,omitempty"`
}

type Employee struct {
Person
ManagerID int
}

type Contractor struct {
Person
CompanyID int
}

func main() {
employees := []Employee{
{
Person: Person{
LastName: "Doe", FirstName: "John", Address: "balabala",
},
},
{
Person: Person{
LastName: "Campbell", FirstName: "David",
},
},
}

//json序列化
data, _ := json.Marshal(employees)
fmt.Printf("%s\n", data)


//反序列化
var decoded []Employee
json.Unmarshal(data, &decoded)
fmt.Printf("%v\n", decoded)
}

//输出结果
[{"ID":0,"name":"John","LastName":"Doe","address":"balabala","ManagerID":0},{"ID":0,"name":"David","LastName":"Campbell","ManagerID":0}]
[{{0 John Doe balabala} 0} {{0 David Campbell } 0}]

序列化详细知识:https://sanyuesha.com/2018/05/07/go-json/

函数

1
2
3
func name(parameters) (results) {
body-content
}

函数类似js的语法

可以带多个返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

package main

import (
"fmt"
"os"
"strconv"
)
func main() {
//从命令行读取两个参数传入calc函数,注意下标不是从0开始
add_sum, mul_sum := calc(os.Args[1], os.Args[2])
fmt.Println(add_sum, mul_sum)
}

//多个返回值的时候标注返回值类型(参数1类型,参数2类型)
func calc(number1 string, number2 string) (int, int) {
//str->int
int1, _ := strconv.Atoi(number1)
int2, _ := strconv.Atoi(number2)
sum := int1 + int2
mul := int1 * int2
return sum, mul
}

指针

  • & 运算符使用其后对象的地址。
  • * 运算符取消引用指针。 也就是说,你可以前往指针中包含的地址访问其中的对象。
1
2
3
4
5
6
7
8
9
10
11
12
package main

func main() {
firstName := "John"
updateName(&firstName)
fmt.println(firstName)
}

func updateName(name *string) {
*name = "David"
}
//将John改成了David

 #### 匿名函数
1、不带参数

1
2
3
4
5
6
7
func main() {
f:=func(){
fmt.Println("hello world")
}
f()//hello world
fmt.Printf("%T\n", f) //打印 func()
}

2、带参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
f:=func(args string){
fmt.Println(args)
}
f("hello world")//hello world
//或
(func(args string){
fmt.Println(args)
})("hello world")//hello world
//或
func(args string) {
fmt.Println(args)
}("hello world") //hello world
}

3、带返回值

1
2
3
4
5
6
7
func main() {
f:=func()string{
return "hello world"
}
a:=f()
fmt.Println(a)//hello world
}

4、多个匿名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
f1,f2:=F(1,2)
fmt.Println(f1(4))//6
fmt.Println(f2())//6
}
func F(x, y int)(func(int)int,func()int) {
f1 := func(z int) int {
return (x + y) * z / 2
}

f2 := func() int {
return 2 * (x + y)
}
return f1,f2
}

闭包(closure)

闭包:说白了就是函数的嵌套,内层的函数可以使用外层函数的所有变量,即使外层函数已经执行完毕

示例:

1、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
a := Fun()
b:=a("hello ")
c:=a("hello ")
fmt.Println(b)//worldhello
fmt.Println(c)//worldhello hello
}
func Fun() func(string) string {
a := "world"
return func(args string) string {
a += args
return a
}
}

2、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
a := Fun()
d := Fun()
b:=a("hello ")
c:=a("hello ")
e:=d("hello ")
f:=d("hello ")
fmt.Println(b)//worldhello
fmt.Println(c)//worldhello hello
fmt.Println(e)//worldhello
fmt.Println(f)//worldhello hello
}
func Fun() func(string) string {
a := "world"
return func(args string) string {
a += args
return a
}
}

注意两次调用F(),维护的不是同一个a变量。

3、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
a := F()
a[0]()//0xc00004c080 3
a[1]()//0xc00004c080 3
a[2]()//0xc00004c080 3
}
func F() []func() {
b := make([]func(), 3, 3)
for i := 0; i < 3; i++ {
b[i] = func() {
fmt.Println(&i,i)
}
}
return b
}

闭包通过引用的方式使用外部函数的变量。例中只调用了一次函数F,构成一个闭包,i 在外部函数B中定义,所以闭包维护该变量 i ,a[0]、a[1]、a[2]中的 i 都是闭包中 i 的引用。因此执行,i 的值已经变为3,故再调用a0时的输出是3而不是0。

4、如何避免上面的BUG ,用下面的方法,注意和上面示例对比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
a := F()
a[0]() //0xc00000a0a8 0
a[1]() //0xc00000a0c0 1
a[2]() //0xc00000a0c8 2
}
func F() []func() {
b := make([]func(), 3, 3)
for i := 0; i < 3; i++ {
b[i] = (func(j int) func() {
return func() {
fmt.Println(&j, j)
}
})(i)
}
return b
}

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
a := F()
a[0]() //0xc00004c080 0
a[1]() //0xc00004c088 1
a[2]() //0xc00004c090 2
}
func F() []func() {
b := make([]func(), 3, 3)
for i := 0; i < 3; i++ {
j := i
b[i] = func() {
fmt.Println(&j, j)
}
}
return b
}

每次 操作仅将匿名函数放入到数组中,但并未执行,并且引用的变量都是 i,随着 i 的改变匿名函数中的 i 也在改变,所以当执行这些函数时,他们读取的都是环境变量 i 最后一次的值。解决的方法就是每次复制变量 i 然后传到匿名函数中,让闭包的环境变量不相同。

5、

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
fmt.Println(F())//2
}
func F() (r int) {
defer func() {
r++
}()
return 1
}

输出结果为2,即先执行r=1 ,再执行r++。

6、递归函数

还有一种情况就是必须用都闭包,就是递归函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func F(i int) int {
if i <= 1 {
return 1
}
return i * F(i-1)
}

func main() {
var i int = 3
fmt.Println(i, F(i))// 3 6
}

7、斐波那契数列(Fibonacci)

这个数列从第3项开始,每一项都等于前两项之和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func fibonaci(i int) int {
if i == 0 {
return 0
}
if i == 1 {
return 1
}
return fibonaci(i-1) + fibonaci(i-2)
}

func main() {
var i int
for i = 0; i < 10; i++ {
fmt.Printf("%d\n", fibonaci(i))
}
}

小结:

匿名函数和闭包其实是一回事儿,匿名函数就是闭包。匿名函数给编程带来灵活性的同时也容易产生bug,在使用过程当中要多注意函数的参数,及可接受的参数的问题。

本地包

go中模块可以用来导入到其他的包中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//定义了一个计算器模块
//首字母大写的函数或者变量可以被外部访问相当于public
//首字母小写的相当于private

package calculator

var logMessage = "[LOG]"

// Version of the calculator
var Version = "1.1"

func internalSum(number int) int {
return number - 1
}

// Sum two integer numbers
func Sum(number1, number2 int) int {
return number1 + number2
}

编写完之后使用 go mod init 给模块取个名字,比如go mod init github.com/myuser/calculator

当前目录下会生成一个go.mod这里是相当于这个”计算器”包的依赖和环境类似py的requirements.txt

依赖然后就可以在main包中调用这个包,使用import

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"github.com/myuser/calculator"
)

func main() {
total := calculator.Sum(3, 5)
fmt.Println(total)
fmt.Println("Version: ", calculator.Version)
}

然后go mod init helloworld,然后生成了这个包的依赖

给go.mod增加几行

1
2
3
4
5

require github.com/myuser/calculator v0.0.0

replace github.com/myuser/calculator => ../calculator

调用本地包

调用第三方包

使用go get xxx来进行第三方包的安装,go get的就是git clone和go install的两个命令的集合

1
2
3
4
5
6
7
8
9
10
package main

import (
"fmt"
"rsc.io/quote"
)

func main() {
fmt.Println(quote.Hello())
}

然后go run main.go即可

有时可能需要重建依赖go mod tidy

vscode中如果导入了第三包出现飘红情况,是打开的文件路径不对,找到和当前项目go.mod所在的目录打开文件夹

流程控制语句

if

if条件表达式可以不带小括号

但是大括号不能省略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func somenumber() int {
return -7
}
func main() {
if num := somenumber(); num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
//这里会报错,因为在if语句块中定义的变量只能在if语句范围中使用,超过范围会报错
fmt.Println(num)
}

switch

在 Go 中,当逻辑进入某个 case 时,它会退出 switch 块(满足条件后退出switch块),除非你显式停止它。

若要使逻辑进入到下一个紧邻的 case,请使用 fallthrough 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

func main() {
switch num := 15; {
//case 可以编写条件
case num < 50:
fmt.Printf("%d is less than 50\n", num)
fallthrough
case num > 100:
fmt.Printf("%d is greater than 100\n", num)
fallthrough
case num < 200:
fmt.Printf("%d is less than 200", num)
}
}

运行代码并分析输出:

输出

1
2
3
15 is less than 50
15 is greater than 100
15 is less than 200

你是否看到错误?

请注意,由于 num 为 15(小于 50),因此它与第一个 case 匹配。 但是,num 不大于 100。 由于第一个 case 语句包含 fallthrough 关键字,因此逻辑会立即转到下一个 case 语句,而不会对该 case 进行验证。 因此,在使用 fallthrough 关键字时必须谨慎。 该代码产生的行为可能不是你想要的。

for

最简例子

1
2
3
4
5
6
7
8
func main() {
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
fmt.Println("sum of 1..100 is", sum)
}

随机数产生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"math/rand"
"time"
)

func main() {
var num int32
sec := time.Now().Unix()
rand.Seed(sec)
//等于while(1)
for {
fmt.Print("Writting inside the loop...")
if num = rand.Int31n(10); num == 5 {
fmt.Println("finish!")
break
}
fmt.Println(num)
}
}

continue中规中矩没啥区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
sum := 0
for num := 1; num <= 100; num++ {
if num%5 == 0 {
continue
}
sum += num
}
fmt.Println("The sum of 1 to 100, but excluding numbers divisible by 5, is", sum)
}

defer panic recover

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

func highlow(high int, low int) {
if high < low {
fmt.Println("Panic!")
// panic可以理解成throw一个异常
panic("highlow() low greater than high")
}
//defer理解成一个堆栈,会在结束的时候逆向重新执行运行
defer fmt.Println("Deferred: highlow(", high, ",", low, ")")
fmt.Println("Call: highlow(", high, ",", low, ")")

highlow(high, low+1)
}

func main() {
defer func() {
handler := recover()
//recover可以理解成catch异常的信息,不显示具体的堆栈跟踪
//如果没有任何错误捕捉到会返回nil
if handler != nil {
fmt.Println("main(): recover", handler)
}
}()
highlow(2, 0)
fmt.Println("Program finished successfully!")
}
1
2
3
4
5
6
7
8
Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
main(): recover highlow() low greater than high

错误处理

错误处理策略

当函数返回错误时,该错误通常是最后一个返回值。 正如上一部分所介绍的那样,调用方负责检查是否存在错误并处理错误。 因此,一个常见策略是继续使用该模式在子例程中传播错误。 例如,子例程(如上一示例中的 getInformation)可能会将错误返回给调用方,而不执行其他任何操作,如下所示:

1
2
3
4
5
6
7
func getInformation(id int) (*Employee, error) {
employee, err := apiCallEmployee(1000)
if err != nil {
return nil, err // Simply return the error to the caller.
}
return employee, nil
}

你可能还需要在传播错误之前添加更多信息。 为此,可以使用 fmt.Errorf() 函数,该函数与我们之前看到的函数类似,但它返回一个错误。 例如,你可以向错误添加更多上下文,但仍返回原始错误,如下所示:

1
2
3
4
5
6
7
func getInformation(id int) (*Employee, error) {
employee, err := apiCallEmployee(1000)
if err != nil {
return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)
}
return employee, nil
}

另一种策略是在错误为暂时性错误时运行重试逻辑。 例如,可以使用重试策略调用函数三次并等待两秒钟,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
func getInformation(id int) (*Employee, error) {
for tries := 0; tries < 3; tries++ {
employee, err := apiCallEmployee(1000)
if err == nil {
return employee, nil
}

fmt.Println("Server is not responding, retrying ...")
time.Sleep(time.Second * 2)
}

return nil, fmt.Errorf("server has failed to respond to get the employee information")
}

最后,可以记录错误并对最终用户隐藏任何实现详细信息,而不是将错误打印到控制台。 我们将在下一模块介绍日志记录。 现在,让我们看看如何创建和使用自定义错误。

创建可重用的错误

有时错误消息数会增加,你需要维持秩序。 或者,你可能需要为要重用的常见错误消息创建一个库。 在 Go 中,你可以使用 errors.New() 函数创建错误并在若干部分中重复使用这些错误,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
//定义错误变量
var ErrNotFound = errors.New("Employee not found!")

func getInformation(id int) (*Employee, error) {
if id != 1001 {
//使用错误变量
return nil, ErrNotFound
}

employee := Employee{LastName: "Doe", FirstName: "John"}
return &employee, nil
}

getInformation 函数的代码外观更优美,而且如果需要更改错误消息,只需在一个位置更改即可。 另请注意,惯例是为错误变量添加 Err 前缀。

最后,如果你具有错误变量,则在处理调用方函数中的错误时可以更具体。 errors.Is() 函数允许你比较获得的错误的类型,如下所示:

1
2
3
4
5
6
7
employee, err := getInformation(1000)
//errors.Is比较错误类型然后补充错误输出
if errors.Is(err, ErrNotFound) {
fmt.Printf("NOT FOUND: %v\n", err)
} else {
fmt.Print(employee)
}

你还需要了解何时使用 panic。 我们已介绍 panicrecover 的工作原理。 仅当明确需要停止程序时,才应使用这些函数。 有时,即使你正确处理了错误,程序也可能会停止响应。 但这应该是异常,而不是规则。

log

log包

  • fatal
  • print
  • painc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import (
"log"
)

func main() {
log.Print("Hey, I'm a log!")
//log.SetPrefix()。 可使用它向程序的日志消息添加前缀
log.SetPrefix("main(): ")
//你可以使用 log.Fatal() 函数记录错误并结束程序,就像使用 os.Exit(1) 一样。
log.Fatal("Hey, I'm an error log!")
// 在使用 log.Panic() 函数时会出现类似行为,该函数也调用 panic() 函数
log.Panic("Hey, I'm an error log!")

}
//output
2021/10/18 09:59:58 Hey, I'm a log!
main(): 2021/10/18 09:59:58 Hey, I'm an error log!
exit status 1
  • 记录到文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package main

    import (
    "log"
    "os"
    )

    func main() {
    file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
    if err != nil {
    log.Fatal(err)
    }

    defer file.Close()

    log.SetOutput(file)
    log.Print("Hey, I'm a log!")
    }

zerolog

重构依赖go mod tidy

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)

func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Print("Hey! I'm a log message!")
}

另一有用功能是你可以快速添加上下文数据,如下所示:

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)

func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix

log.Debug().
Int("EmployeeID", 1001).
Msg("Getting employee information")

log.Debug().
Str("Name", "John").
Send()
}

运行前面的代码时,将看到以下输出:

输出

1
2
{"level":"debug","EmployeeID":1001,"time":1609855731,"message":"Getting employee information"}
{"level":"debug","Name":"John","time":1609855731}

method(oop)

usage:

1
2
3
func (variable type) MethodName(parameters ...) {
// method functionality
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import "fmt"

type triangle struct {
size int
}

type coloredTriangle struct {
triangle
color string
}

type square struct {
size int
}

//方法对于go来说就是特殊的一个函数,即函数名之前有一个括号里注明属于的结构体(对象)
func (t triangle) perimeter() int {
return t.size * 3
}

// overload 重载上面的函数
func (t coloredTriangle) perimeter() int {
return t.size * 3 * 2
}

func (s square) perimeter() int {
return s.size * 4
}

func main() {
//有点类似构造函数初始化时直接赋值
t := triangle{3}
t1 := coloredTriangle{triangle{3}, "blue"}
s := square{4}

fmt.Println("Perimeter (triangle):", t.perimeter())
fmt.Println("Perimeter (triangle colored):", t1.size, t1.perimeter())
fmt.Println("Perimeter (normal)", t1.triangle.perimeter())
fmt.Println("Perimeter (square):", s.perimeter())
}
//output
Perimeter (triangle): 9
Perimeter (triangle colored): 3 18
Perimeter (normal) 9
Perimeter (square): 16

interface(oop)

Go 中的接口是一种用于表示其他类型的行为的数据类型。 接口类似于对象应满足的蓝图或协定。 在你使用接口时,你的基本代码将变得更加灵活、适应性更强,因为你编写的代码未绑定到特定的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import (
"fmt"
"math"
)

// 定义接口
type Shape interface {
Perimeter() float64
Area() float64
}

type Square struct {
size float64
}

type Circle struct {
radius float64
}

// “矩形”的接口实现
func (s Square) Area() float64 {
return s.size * s.size
}

func (s Square) Perimeter() float64 {
return s.size * 4
}

// ”圆形“的接口实现
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.radius
}

// 打印信息
func printInformation(s Shape) {
fmt.Printf("%T\n", s)
fmt.Println("Area: ", s.Area())
fmt.Println("Perimeter:", s.Perimeter())
fmt.Println()
}

// 使用接口的优点在于,对于 Shape的每个新类型或实现,printInformation 函数都不需要更改
func main() {
var s Shape = Square{3}
printInformation(s)

var c Shape = Circle{6}
printInformation(c)
}

扩展现有实现

假设你具有以下代码,并且希望通过编写负责处理某些数据的 Writer 方法的自定义实现来扩展其功能。

通过使用以下代码,你可以创建一个程序,此程序使用 GitHub API 从 Microsoft 获取三个存储库:

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"io"
"net/http"
"os"
)

func main() {
resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}

io.Copy(os.Stdout, resp.Body)
}

运行前面的代码时,你会收到类似于以下输出的内容(已缩短以便改善可读性):

输出

1
2
[{"id":276496384,"node_id":"MDEwOlJlcG9zaXRvcnkyNzY0OTYzODQ=","name":"-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","full_name":"microsoft/-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","private":false,"owner":{"login":"microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/microsoft","html_url":"https://github.com/micro
....

请注意,io.Copy(os.Stdout, resp.Body) 调用是指将通过对 GitHub API 的调用获取的内容打印到终端。 假设你想要写入自己的实现以缩短你在终端中看到的内容。 在查看 io.Copy 函数的源 时,你将看到以下内容:

Go

1
func Copy(dst Writer, src Reader) (written int64, err error)

如果你深入查看第一个参数 dst Writer 的详细信息,你会注意到 Writer接口

Go

1
2
3
type Writer interface {
Write(p []byte) (n int, err error)
}

你可以继续浏览 io 程序包的源代码,直到找到 Copy 调用 Write 方法的位置。 我们暂时不做任何处理。

由于 Writer 是接口,并且是 Copy 函数需要的对象,你可以编写 Write 方法的自定义实现。 因此,你可以自定义打印到终端的内容。

实现接口所需的第一项操作是创建自定义类型。 在这种情况下,你可以创建一个空结构,因为你只需按如下所示编写自定义 Write 方法即可:

Go

1
type customWriter struct{}

现在,你已准备就绪,可开始编写自定义 Write 函数。 此时,你还需要编写一个结构,以便将 JSON 格式的 API 响应解析为 Golang 对象。 你可以使用“JSON 转 Go”站点从 JSON 有效负载创建结构。 因此,Write 方法可能如下所示:

Go

1
2
3
4
5
6
7
8
9
10
11
12
type GitHubResponse []struct {
FullName string `json:"full_name"`
}

func (w customWriter) Write(p []byte) (n int, err error) {
var resp GitHubResponse
json.Unmarshal(p, &resp)
for _, r := range resp {
fmt.Println(r.FullName)
}
return len(p), nil
}

最后,你必须修改 main() 函数以使用你的自定义对象,具体如下所示:

Go

1
2
3
4
5
6
7
8
9
10
func main() {
resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}

writer := customWriter{}
io.Copy(writer, resp.Body)
}

在运行程序时,你将会看到以下输出:

输出

1
2
3
4
5
microsoft/aed-blockchain-learn-content
microsoft/aed-content-nasa-su20
microsoft/aed-external-learn-template
microsoft/aed-go-learn-content
microsoft/aed-learn-template

由于你写入的自定义 Write 方法,输出效果现在更好。 以下是程序的最终版本:

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)

type GitHubResponse []struct {
FullName string `json:"full_name"`
}

type customWriter struct{}

func (w customWriter) Write(p []byte) (n int, err error) {
var resp GitHubResponse
json.Unmarshal(p, &resp)
for _, r := range resp {
fmt.Println(r.FullName)
}
return len(p), nil
}

func main() {
resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}

writer := customWriter{}
io.Copy(writer, resp.Body)
}

web api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"log"
"net/http"
)

type dollars float32

func (d dollars) String() string {
return fmt.Sprintf("$%.2f", d)
}

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}

func main() {
db := database{"Go T-Shirt": 25, "Go Jacket": 55}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
//访问网站会出现
Go Jacket: $55.00
Go T-Shirt: $25.00

并发

无缓冲channel

无缓冲 channel 同步通信。 它们保证每次发送数据时,程序都会被阻止,直到有人从 channel 中读取数据。

无缓冲匿名函数实现并发打印1-10

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
ch := make(chan int)
for i := 0; i < 10; i++ {
go func(n int) {
ch <- n
return
}(i)
}
for j := 0; j < 10; j++ {
fmt.Print(<-ch)
}
}

无缓冲非匿名函数实现并发打印1-10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
ch := make(chan int)
for i := 0; i < 10; i++ {
go run_10(i, ch)
}
for j := 0; j < 10; j++ {
fmt.Print(<-ch)
}
}

func run_10(n int, ch chan int) {
ch <- n
return
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
"fmt"
"net/http"
"time"
)

func main() {
start := time.Now()

apis := []string{
"https://management.azure.com",
"https://dev.azure.com",
"https://api.github.com",
"https://outlook.office.com/",
"https://api.somewhereintheinternet.com/",
"https://graph.microsoft.com",
}
// 创建一个channel用于传递并发之间的消息
ch := make(chan string)

for _, api := range apis {
//让checkAPI并发运行,发送channel
go checkAPI(api, ch)
}
// channel返回了apis数量的消息,需要全部接收下来
for i := 0; i < len(apis); i++ {
//接收channel
fmt.Print(<-ch)
}

elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

func checkAPI(api string, ch chan string) {
_, err := http.Get(api)
if err != nil {
// 向channel返回消息,相当于返回值通过channel传送回去了
ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
return
}

ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

有缓冲 channel

将发送和接收操作解耦。 它们不会阻止程序,但你必须小心使用,因为可能最终会导致死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
size := 2
ch := make(chan string, size)
send(ch, "one")
send(ch, "two")
//如果没有go会导致死锁,创建goroutine(使用go关键字)就不会产生这个问题
go send(ch, "three")
go send(ch, "four")
fmt.Println("All data sent to the channel ...")

for i := 0; i < 4; i++ {
fmt.Println(<-ch)
}

fmt.Println("Done!")
}

channel方向

1
2
chan<- int // it's a channel to only send data
<-chan int // it's a channel to only receive data

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"
//无缓存的channel不存在发送和接收数据
// chan<- string是只发送的channel
func send(ch chan<- string, message string) {
fmt.Printf("Sending: %#v\n", message)
ch <- message
}

//<-chan是只接收的channel
func read(ch <-chan string) {
fmt.Printf("Receiving: %#v\n", <-ch)
}

func main() {
ch := make(chan string, 1)
send(ch, "Hello World!")
read(ch)
}

多路复用

最后,让我们讨论一个关于如何在使用 select 关键字的同时与多个 channel 交互的简短主题。 有时,在使用多个 channel 时,需要等待事件发生。 例如,当程序正在处理的数据中出现异常时,可以包含一些逻辑来取消操作。

select 语句的工作方式类似于 switch 语句,但它适用于 channel。 它会阻止程序的执行,直到它收到要处理的事件。 如果它收到多个事件,则会随机选择一个。

select 语句的一个重要方面是,它在处理事件后完成执行。 如果要等待更多事件发生,则可能需要使用循环。

让我们使用以下程序来看看 select 的运行情况:

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"time"
)

func process(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "Done processing!"
}

func replicate(ch chan string) {
time.Sleep(1 * time.Second)
ch <- "Done replicating!"
}

func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go process(ch1)
go replicate(ch2)

for i := 0; i < 2; i++ {
select {
case process := <-ch1:
fmt.Println(process)
case replicate := <-ch2:
fmt.Println(replicate)
}
}
}

运行程序时,将看到以下输出:

输出

1
2
Done replicating!
Done processing!

请注意,replicate 函数先完成。 这就是你在终端中先看到其输出的原因。 main 函数存在一个循环,因为 select 语句在收到事件后立即结束,但我们仍在等待 process 函数完成。

通过并发实现fib数列(一个有缓存channel)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"fmt"
"math/rand"
"time"
)

func fib(number float64, ch chan string) {
x, y := 1.0, 1.0
for i := 0; i < int(number); i++ {
// x变成x+y,循环保证了只需要最后一个
x, y = y, x+y
}

r := rand.Intn(3)
time.Sleep(time.Duration(r) * time.Second)

ch <- fmt.Sprintf("Fib(%v): %v\n", number, x)
}

func main() {
start := time.Now()

size := 15
ch := make(chan string, size)

for i := 0; i < size; i++ {
go fib(float64(i), ch)
}

for i := 0; i < size; i++ {
//只能按照随机的顺序打印,因为不知道是哪个进程先完成的
fmt.Printf(<-ch)
}

elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

输入quit退出运算,不然进行fib的计算(两个无缓存channel)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"time"
)

var quit = make(chan bool)

func fib(c chan int) {
x, y := 1, 1

for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("Done calculating Fibonacci!")
return
}
}
}

func main() {
start := time.Now()

command := ""
data := make(chan int)

go fib(data)

for {
num := <-data
fmt.Println(num)
fmt.Scanf("%s", &command)
if command == "quit" {
quit <- true
break
}
}

time.Sleep(1 * time.Second)

elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

sync.waitGroup并发

实际使用中用的最多的还是这个并发方式,用起来很方便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
	var wg sync.WaitGroup
//定义几个并发的协程
worker := make(chan struct{}, 10)
count, _ := GetTop10kCount()

var i int64
//列队中的并发任务
for i = 1; i < count; i++ {

worker <- struct{}{}
wg.Add(1)

imaven := model.Imaven{Id: i}
//创建一个匿名函数来执行并发
go func(m model.Imaven) {
defer func() {
//执行协程
<-worker
wg.Done()
}()
//单个协程并并发的业务逻辑
_, err := global.Top10kEngine.Table("i_maven").Get(&imaven)
if err != nil {
panic(err)
}
//detect gav is exist,if exist do nothing,else write db
if has, _ := global.Engine.Exist(&model.Jarhash{
GroupId: imaven.GroupId,
ArtifactId: imaven.ArtifactId,
Version: imaven.Version,
}); has {
fmt.Println(imaven.GroupId, imaven.ArtifactId, imaven.Version, " is exists")
return
} else {
WriteJarHash2DB(global.Client.R(), imaven.GroupId, imaven.ArtifactId, imaven.Version)
}

}(imaven)

}
//如果达到十个就等待
wg.Wait()

测试

测试文件以name_test.go命名

1
2
3
4
5
import "testing"

func TestAccount(t *testing.T) {

}

go test -v

检测是否通过测试

第三方模块推荐

gin框架

https://geektutu.com/post/quick-go-gin.html

https://laravelacademy.org/post/21861

xorm

文档:

https://gitea.com/xorm/xorm/src/branch/master/README_CN.md

gotoml

用来读配置文件的第三方包

第三方包”github.com/pelletier/go-toml”

go get 一下然后import

假设有配置文件config.toml

1
2
[db]
host = "localhost"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// read the config.toml and set the value
path, err := filepath.Abs("config.toml")
if err != nil {
panic(err)
}

config, err := toml.LoadFile(path)
if err != nil {
panic(err)
}

var none bool

db_host := config.Get("db.host")
if v, ok := db_host.(string); ok {
dbHOST = v
} else {
none = true
}

// if the config is not complete, panic the err
if none {
panic("the config is not complete")
}


resty

用于发送和处理http请求的

https://github.com/go-resty/resty

gjson

用于处理json数据的

https://github.com/tidwall/gjson