0x01go总结摘抄与记录| 青训营笔记

147 阅读20分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

本篇文章主要摘要了我觉得与我熟悉的语言(c++,python,java)在语法以及其他特性上的区别与各种语法的用法与注意事项

来源:前言 · Go语言圣经;青训营课堂

go的特性

  • 编译性语言
  • 通过包组织,每个源文件都是以一条package声明开始,紧跟着一系列导入的包
    • fmt包包含格式化输出,输入
    • main包比较特殊,定义了一个独立可执行的程序;其中的main函数是整个程序执行的入口
  • 必须导入恰当的包,不能多也不能少
  • 函数对数据结构的引用是类似引用传递,修改会落实

go的特点

  • 高性能、高并发
  • 语法简单
  • 标准库丰富
  • 完善工具链
  • 静态链接
  • 快速编译
  • 跨平台
  • 垃圾回收

运行与编译

运行使用run命令,编译成二进制为build

go build 
go run

程序结构

变量

变量会在声明时直接初始化,如果没有显示初始化,则被隐式的赋予其类型的零值,数值为0,字符串为空字符串

名字的开头字母的大小写决定了名字在包外的可见性

如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问。例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。包本身的名字一般总是用小写字母。

声明

var const type func,分别对应变量、常量、类型、函数实体对象

var声明一个特定类型的变量,var 变量名 类型 = 表达式

其中“类型”或“= 表达式”两个部分可以省略其中的一个;省略类型自动推导;省略表达式赋为对应的类型的零值

值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil

数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值

Go语言程序员应该让一些聚合类型的零值也具有意义,这样可以保证不管任何类型的变量总是有一个合理有效的零值状态。

var i, j, k int                 // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string

:=短变量声明,定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句,类型通过表达式自动推导

简短变量声明被广泛用于大部分的局部变量的声明和初始化,var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方

指针

一个指针的值是另一个变量的地址

&x表示一个指向该证书变量的指针,对应的数据类型是*int(如果他是int的话)

如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”

*p表达式对应p指针指向的变量的值

指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等

new

表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T

变量声明周期

对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的

局部变量的生命周期:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收

一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的

赋值

支持++ --

map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边时,并不一定是产生两个结果,也可能只产生一个结果。对于只产生一个结果的情形,map查找失败时会返回零值,类型断言失败时会发生运行时panic异常,通道接收失败时会返回零值(阻塞不算是失败)。例如下面的例子:(暂时不懂)

v = m[key]                // map查找,失败时返回零值
v = x.(T)                 // type断言,失败时panic异常
v = <-ch                  // 管道接收,失败时返回零值(阻塞不算是失败)

_, ok = m[key]            // map返回2个值
_, ok = mm[""], false     // map返回1个值
_ = mm[""]                // map返回1个值

类型

新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的

不同类型不能比较

type 类型名字 底层类型

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

在函数返回对应类型的时候,需要显示的转型操作T(x)用于将x转换为T类型

包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:

var a = b + c // a 第三个初始化, 为 3
var b = f()   // b 第二个初始化, 为 2, 通过调用 f (依赖c)
var c = 1     // c 第一个初始化, 为 1

func f() int { return c + 1 }

如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译

数据类型

整型

位操作运算符^作为二元运算符时是按位异或(XOR),当用作一元运算符时表示按位取反;

浮点数到整数的转换将丢失任何小数部分,然后向数轴零方向截断

复数

Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部:

如果一个浮点数面值或一个十进制整数面值后面跟着一个i,例如3.141592i或2i,它将构成一个复数的虚部

布尔

布尔值并不会隐式转换为数字值0或1,反之亦然。必须使用一个显式的if语句辅助转换

字符串

一个字符串是一个不可改变的字节序列

内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目),索引操作s[i]返回第i个字节的字节值,i必须满足0 ≤ i< len(s)条件约束。

因为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的:

因为Go语言源文件总是用UTF8编码,并且Go语言的文本字符串也以UTF8编码的方式处理,因此我们可以将Unicode码点也写到字符串面值中

字符串函数

字符串函数,基本都在strings,strconv中

n, err := strconv.Atoi(“12”) // 转整数
str = strconv.Itoa(12345) // 转字符串
f, _ := strconv.ParseFloat("1.234", 64) // 转为64位浮点数
n, _ := strconv.ParseInt("111", 10, 64) // 转为10进制的64位整数
n, _ = strconv.ParseInt("0x1000", 0, 64) // 中间为0代表自动推测
str = strconv.FormatInt(123, 2) // 10进制转 2,8,16进制
strings.Contains(“seafood”, “foo”) // 查找子串是否在指定的字符串中,返回bool
strings.Count(“ceheese”, “e”) // 统计一个字符串有几个指定的子串
strings.Replace(“go go hello”, “go”, “go语言”,n) // n可以指定用户希望替换几个,如果n= -1 将表示全部替换
strings.TrimSpace(" tn a lone good noe ") // 去除两边的空格
strings.Split("a-b-c", "-") // 以'-'分隔

格式化

Printf可以统一使用%v来进行占位

使用%+v%#v能够打印更详细的信息

常量

iota 常量生成器

在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。

下面是来自time包的例子,它首先定义了一个Weekday命名类型,然后为一周的每天定义了一个常量,从周日0开始。在其它编程语言中,这种类型一般被称为枚举类型。

type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

周日将对应0,周一为1,如此等等。

无类型常量

编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算;你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串

通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。例如,例子中的ZiB和YiB的值已经超出任何Go语言中整数类型能表达的范围,但是它们依然是合法的常量,而且像下面的常量表达式依然有效

fmt.Println(YiB/ZiB) // "1024"

另一个例子,math.Pi无类型的浮点数常量,可以直接用于任意需要浮点数或复数的地方:

var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

前面说过除法运算符/会根据操作数的类型生成对应类型的结果。因此,不同写法的常量除法表达式可能对应不同的结果:

var f float64 = 212
fmt.Println((f - 32) * 5 / 9)     // "100"; (f - 32) * 5 is a float64
fmt.Println(5 / 9 * (f - 32))     // "0";   5/9 is an untyped integer, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float

只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,就像下面的第一行语句,或者出现在有明确类型的变量声明的右边,如下面的其余三行语句,无类型的常量将会被隐式转换为对应的类型

对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型,就像下面的例子:

i := 0      // untyped integer;        implicit int(0)
r := '\000' // untyped rune;           implicit rune('\000')
f := 0.0    // untyped floating-point; implicit float64(0.0)
c := 0i     // untyped complex;        implicit complex128(0i)

复合数据类型

数组

var a [3]int             // array of 3 integers

for i, v := range a {
    fmt.Printf("%d %d\n", i, v)
}

// Print the elements only.
for _, v := range a {
    fmt.Printf("%d\n", v)
}

默认情况下,数组的每个元素都被初始化为元素类型对应的零值

初始化

var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"

在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算

q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"

也可以指定一个索引和对应值列表的方式初始化,就像下面这样:

type Currency int

const (
    USD Currency = iota // 美元
    EUR                 // 欧元
    GBP                 // 英镑
    RMB                 // 人民币
)

symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

fmt.Println(RMB, symbol[RMB]) // "3 ¥"
// 这里相当于是RMB映射索引

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则

Slice

一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象

一个slice由三个部分构成:指针长度容量

指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。

长度对应slice中元素的数目;长度不能超过容量,

容量一般是从slice的开始位置到底层数据的结尾位置。

内置的lencap函数分别返回slice的长度和容量。

多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠

字符串的切片操作和[]byte字节类型切片的切片操作是类似的。都写作x[m:n],并且都是返回一个原始字节序列的子序列,底层都是共享之前的底层数组,因此这种操作都是常量时间复杂度

因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别

不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较

如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断

内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。

定义

months := [...]string{1: "January", /* ... */, 12: "December"}

相关函数

Copy

内置的copy函数可以方便地将一个slice复制另一个相同类型的slice。copy函数的第一个参数是要复制的目标slice,第二个参数是源slice,目标和源的位置顺序和dst = src赋值语句是一致的。两个slice可以共享同一个底层数组,甚至有重叠也没有问题。copy函数将返回成功复制的元素的个数(我们这里没有用到),等于两个slice中较小的长度,所以我们不用担心覆盖会超出目标slice的范围。

append

我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量:

runes = append(runes, r)

我们的appendInt函数每次只能向slice追加一个元素,但是内置的append函数则可以追加多个元素,甚至追加一个slice。

var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) // append the slice x
fmt.Println(x)      // "[1 2 3 4 5 6 1 2 3 4 5 6]"

Map

map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型

K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在

内置的make函数可以创建一个map:

ages := make(map[string]int) // mapping from strings to ints

我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:

ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}

使用内置的delete函数可以删除元素:

delete(ages, "alice") // remove element ages["alice"]

要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,和之前的slice遍历语法类似。下面的迭代语句将在每次迭代时设置name和age变量,它们对应下一个键/值对:

for name, age := range ages {
    fmt.Printf("%s\t%d\n", name, age)
}

结构体

下面两个语句声明了一个叫Employee的命名的结构体类型,并且声明了一个Employee类型的变量dilbert:

type Employee struct {
    ID        int
    Name      string
    Address   string
    DoB       time.Time
    Position  string
    Salary    int
    ManagerID int
}

var dilbert Employee

// 初始化
a := Employee{ID: 123, Name: "xiaoming"} // 指定赋值
b := Employee{456, "xiaohong"} // 按结构体定义的顺序
// 未赋值的设为空值

也可以通过.来获取和修改

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。

如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回,

func Bonus(e *Employee, percent int) int {
    return e.Salary * percent / 100
}

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的

同样的,函数传入结构体指针可以对这个结构体进行操作,并且避免一些大结构体拷贝的开销

结构体方法

func (u user) checkPassword(password string) bool { // 结构特点
	return u.password == password
}

func (u *user) resetPassword(password string) {
	u.password = password
}

func main() {
	a := user{name: "wang", password: "1024"}
	a.resetPassword("2048")
	fmt.Println(a.checkPassword("2048")) // true
}

range

使用range能快速遍历数组或者slice,使代码更加简洁

for i, num := range nums { // 对数组遍历会返回两个值,第一个值是索引,第二个值是对应的值
    
}

for k, v := range mapp { // 与上面类似,获取kv
    
}

json

将结构体变为json,只需要结构体中的所有元素首字母大写(可导出的),然后使用json.Marshal()序列化

就变成了byte数组

type userInfo struct {
	Name  string
	Age   int `json:"age"` // 加一个json的tag,输出就变成小写的了
	Hobby []string
}

func main() {
	a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
	buf, err := json.Marshal(a)
	if err != nil {
		panic(err)
	}
	fmt.Println(buf)         // [123 34 78 97...]
	fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}

	buf, err = json.MarshalIndent(a, "", "\t") // 用来格式化输出
	if err != nil {
		panic(err)
	}
	fmt.Println(string(buf))

	var b userInfo
	err = json.Unmarshal(buf, &b) // 反序列化到空的结构体中
	if err != nil {
		panic(err)
	}
	fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}

时间

time库

time.Now()获取当前时间

now := time.Now()
fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC) // 构造带时区的时间
t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
fmt.Println(t)                                                  // 2022-03-27 01:25:36 +0000 UTC
fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 获取时间信息 2022 March 27 1 25
fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 按照括号里的样式进行格式化,必须用2006-01-02 15:04:05的这个时间来进行格式化 2022-03-27 01:25:36
diff := t2.Sub(t) // 对时间进行减法得到时间段
fmt.Println(diff)                           // 1h5m0s
fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
if err != nil {
    panic(err)
}
fmt.Println(t3 == t)    // true
fmt.Println(now.Unix()) // 获取时间戳 1648738080

程序结构

选择结构

if

选择结构主要应用的还是if,当然go中也存在swichselect结构

if 判断表达式 {
    
} else if 判断表达式 {
    
} else {
    
}

并且由于go的码风要求,必须按照上述要求来写,注意判断中不要带括号

switch

go中的switch与其他语言的区别是对于其中的case是不需要加入break的,go中的case执行完后不会往下继续执行其他的case

并且a的类型并不只限于整数类型

	switch a {
	case 1:
		fmt.Println("one")
	case 2:
		fmt.Println("two")
	case 3:
		fmt.Println("three")
	case 4, 5:
		fmt.Println("four or five")
	default:
		fmt.Println("other")
	}

循环结构

go中只有for循环,但是他同样实现了其他语言的while循环

go语言有三种结构

for [true]{ // 死循环,方括号代表可选
    
}

for i := 1; i < 5; i++ { // 常规for循环
    
}

for i <= 3{ // 类似while循环
    
}

同样的,go中也有众多语言都有的continuebreak

函数

一般的业务逻辑都会放两个值,第一个是真正的值,第二个是错误信息

func functionName(a []int, b string,c float64) (d int, ok bool) {
    // 函数体
    return d, ok
}

注意:函数中传入数据类型(如slice,没有结构体)会是引用传递,也就是修改会落实;其他类型的只是拷贝一个值

错误处理

go语言的错误处理的习惯是单独返回一个返回值

func functionName (参数) (返回值1, 返回值2, err error){
    // 在所有要返回的地方都要返回错误信息,对的返回正确的错误信息
}

例子:来源 课件

func findUser(users []user, name string) (v *user, err error) {
	for _, u := range users {
		if u.name == name {
			return &u, nil
		}
	}
	return nil, errors.New("not found")
}

func main() {
	u, err := findUser([]user{{"wang", "1024"}}, "wang")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(u.name) // wang

	if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
		fmt.Println(err) // not found
		return
	} else {
		fmt.Println(u.name)
	}
}

环境相关

// go run example/20-env/main.go a b c d
fmt.Println(os.Args)           // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
// 打印运行命令参数
fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin... 查看环境变量
fmt.Println(os.Setenv("AA", "BB")) // 写入环境变量

buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput() // 启动命令并获取输入输出
if err != nil {
    panic(err)
}
fmt.Println(string(buf)) // 127.0.0.1       localhost