这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天
一、本堂课重点内容
-
熟练使用切片
-
Map 无序
-
Range 遍历
-
极为有限的指针(变量, 函数, 方法)
-
错误处理
-
字符串处理库
stringsfmt%v -
Json 处理
-
时间库
-
Atoi (strconv)
-
获取命令行参数 (flag)
二、详细知识点介绍:
格式化
Go 语言可以使用 %v 很是方便
fmt.Printf("type %T value %v \n", a, a)
循环
Go 里的 Switch 不需要 break, 而且可以使用任意类型
- 除了 case 之外, 可以使用 default 作为默认匹配情况
- 可以使用
,匹配多种情况
if num := 10; num > 0 {
fmt.Println(num)
}
for i := 1; i < 10; i++ {
fmt.Println(i)
if i == 3 {
break
}
}
finger := 2
switch finger {
case 1:
fmt.Println(1)
case 2:
fmt.Println(2)
fallthrough
default:
println("sss")
}
dd := 1
switch {
case dd > 0:
println(0)
case dd > -1:
println(-1)
}
数组
数组有固定大小,数组的大小是类型的一部分,因此 [5]int 和 [25]int 是不同类型
a := []int{2}
a[0] = 1
fmt.Println(a)
var b [3][2]int
b = [3][2]int{
{1, 2},
{1, 2},
{1, 2},
}
fmt.Println(b)
veggies := []string{"potatoes", "tomatoes", "brinjal"}
fruits := []string{"oranges", "apples"}
food := append(veggies, fruits...)
fmt.Println("food:", veggies, fruits, food)
fmt.Println("food:", cap(veggies), cap(fruits), cap(food))
fmt.Println("food:", len(veggies), len(fruits), len(food))
nums := []int{1, 2}
change(nums...)
pln(nums...)
map
- map 的零值为 nil,必须使用 make 初始化
- map 是 引用类型,当 map 被赋值给另一个变量是后,他们共享一个
- map 不能使用==判断,==只能用来判断 map 是否为 nil,应该遍历字典元素去比较两个字典
- map 是无序的
var mm map[string]int
- mm["s"] = 1 // 回报错,map is nil
- fmt.Printf("%T %v \n", mm, mm) // 这里虽然能打印出 map[],但是无济于事
if mm == nil {
mm = make(map[string]int)
mm["s"] = 1
fmt.Printf("%T %v \n", mm, mm)
}
mmm := map[string]int{
"aaa": 1,
}
if v, ok := mmm["aa"]; ok == true {
fmt.Println(v)
delete(mmm, "aa")
} else {
fmt.Println("no such key")
fmt.Println(len(mmm))
for k, v := range mmm {
fmt.Println(k, v)
}
}
字符串 与 切片
// 字符串
name := "Señor"
for i := 0; i < len(name); i++ {
fmt.Printf("%c", name[i])
}
fmt.Printf("\n")
for _, v := range name {
fmt.Printf("%c", v)
}
fmt.Printf("\n")
name_ := []rune(name)
for i := 0; i < len(name_); i++ {
fmt.Printf("%c", name_[i])
}
当对切片调用append(slice, ...elems)是,如果超出切片的 cap,就会重新分配内存空间,因此必须需要用变量接受返回值
关于切片
- a[x] 是 (*a)[x] 的简写形式
- arr := [3]int{1, 2, 3}
- modify(&arr)
- modify(arr[:]) 这种更常用
- arr++ 这种直接进行指针操作不被允许
func modify1(arr *[3]int) {
(*arr)[0] = 90
}
func modify2(arr *[3]int) {
arr[0] = 90
}
func change(elems ...int) {
for i, v := range elems {
v += 1 // 无效
elems[i] += 1 // 有效
}
}
func pln(elems ...int) {
for i, v := range elems {
fmt.Printf("index: %v value %v\n", i, v)
}
}
结构体
- 结构体是值类型。如果它的每一个字段都是可比较的,则该结构体也是可比较的。如果两个结构体变量的对应字段相等,则这两个变量也是相等的。
- 如果结构体包含不可比较的字段,则结构体变量也不可比较。
书写
-
匿名结构体:string,int 就是字段名,字段不能重复
type Person struct { string int } person := Person{"aa", 1} fmt.Println(person.int, person.string) -
提升字段:嵌入的结构体,可以直接调用里面的字段
type Group struct { string int Person } -
匿名 + 提升,向上面的情况
匿名的类型可以重复,但是会以自身的为准
group := Group{"bb", 1, person} fmt.Print(group.string, group.string)
结构体 Tag
格式 空格分割的键值对
使用 示例:json 库能够反序列化结构体
- 如果加上 omitepty,当结构体为空是就会被忽略
- 如果不加,为空的字段会被解析为空字符串""
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Addr string `json:"addr"` // ,omitempty
}
func main() {
p1 := Person{
Name: "Jack",
Age: 22,
}
data1, _ := json.Marshal(p1)
fmt.Printf("%s\n", data1)
}
可以通过反射读取 tag
// 三种获取 field
field := reflect.TypeOf(obj).FieldByName("Name")
field := reflect.ValueOf(obj).Type().Field(i) // i 表示第几个字段
field := reflect.ValueOf(&obj).Elem().Type().Field(i) // i 表示第几个字段
// 获取 Tag
tag := field.Tag
// 获取键值对
labelValue := tag.Get("label") // 获取不到就会返回 ""
labelValue,ok := tag.Lookup("label")
- 获取键值对,有 Get 和 Lookup 两种方法,但其实 Get 只是对 Lookup 函数的简单封装而已,当没有获取到对应 tag
的内容,会返回空字符串。
func (tag StructTag) Get(key string) string { v, _ := tag.Lookup(key) return v } - 空 Tag 和不设置 Tag 效果是一样的
方法 & 函数
结构体上的方法
- 结构体方法:不管是一个值,还是一个可以解引用的指针,调用这样的方法都是合法的。或者说:用一个指针或者一个可取得地址的值来调用都是合法的
- 匿名字段的方法:属于结构体的匿名字段的方法可以被直接调用,就好像这些方法是属于定义了匿名字段的结构体一样。
type rectangle struct {
length int
width int
}
func (r *rectangle) area() {
r.length += 1
}
// func (r rectangle) area() {
// r.length += 1
// }
r := rectangle{
length: 10,
width: 5,
}
r.area()
(&r).area()
// {12, 5}
// 如果改为 不带*的方法,{10, 5}
那么什么时候使用指针接收器,什么时候使用值接收器?
- 一般来说,指针接收器可以使用在:对方法内部的接收器所做的改变应该对调用者可见时。
- 指针接收器也可以被使用在如下场景:当拷贝一个结构体的代价过于昂贵时。考虑下一个结构体有很多的字段。在方法内使用这个结构体做为值接收器需要拷贝整个结构体,这是很昂贵的。在这种情况下使用指针接收器,结构体不会被拷贝,只会传递一个指针到方法内部使用。
- 在其他的所有情况,值接收器都可以被使用。
孤儿规则🐶
下面的不允许,因为 int 类型和这个方法,不再同一个包里
func (a int) add(b int) {
}
解决方法
- 定义类型别名
type myInt int
func (a myInt) add(b myInt) myInt {
return a + b
}
- wrapper 包装
函数
匿名函数
func main() {
func(n string) {
fmt.Println("Welcome", n)
}("Gophers")
}
自定义函数类型
type add func(a int, b int) int
func main() {
var a add = func(a int, b int) int {
return a + b
}
s := a(5, 6)
fmt.Println("Sum", s)
}
高阶函数
// 接受函数
func simple(a func(a, b int) int) {
fmt.Println(a(60, 7))
}
// 返回函数
func simple() func(a, b int) int {
f := func(a, b int) int {
return a + b
}
return f
}
接口
类似于 dyn Train
type SalaryCalculator interface {
CalculateSalary() int
}
employees := []SalaryCalculator{pemp1, pemp2, cemp1}
接口的断言
- 类型断言
func assert(i interface{}) {
v, ok := i.(int)
fmt.Println(v, ok)
// 如果不是 int 类型,v 就会被赋为 T 的零值
}
func main() {
var s interface{} = 56
assert(s)
var i interface{} = "Steven Paul"
assert(i)
}
- switch type 注意:把变量传递到函数中后会自动转换类型到 interface,因此调用函数能行,但是下面直接 switch 就不行
- 回报错:a (variable of type int) is not an interface
func findType(i interface{}) {
switch i.(type) {
case string:
fmt.Printf("I am a string and my value is %s\n", i.(string))
case int:
fmt.Printf("I am an int and my value is %d\n", i.(int))
default:
fmt.Printf("Unknown type\n")
}
}
func main() {
a := 1
findType(a)
switch interface{}(a).(type) {
case string:
fmt.Printf("I am a string and my value is %s\n", interface{}(a).(string))
case int:
fmt.Printf("I am an int and my value is %d\n", interface{}(a).(int))
default:
fmt.Printf("Unknown type\n")
}
}
接口类型变量
声明一个变量是接口类型,那么这个变量可以被赋值为,任何实现了接口的类型
type Describer interface {
Describe()
}
type Person struct {
name string
age int
}
type Address struct {
state string
country string
}
现在还不能赋值,接下来为两个 struct 我们实现接口
为 person 实现 describe,使用值接受者,下面两种赋值都可以,也都能调用方法
func (p Person) Describe() { // 使用值接受者实现
fmt.Printf("%s is %d years old\n", p.name, p.age)
}
var d1 Describer
p1 := Person{"Sam", 25}
d1 = p1
d1.Describe()
p2 := Person{"James", 32}
d1 = &p2
d1.Describe()
为 Address 实现 describer,使用指针接受者,下面就比较特殊 d = a 不能直接赋值,如果是在结构体的方法中,下面的两种赋值都是可以的,但是在接口中不行
其原因是:对于使用指针接受者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。但接口中存储的具体值(Concrete Value)并不能取到地址,因此在下面的例子中,对于编译器无法自动获取 a 的地址,于是程序报错。
func (a *Address) Describe() { // 使用指针接受者实现
fmt.Printf("State %s Country %s", a.state, a.country)
}
var d Describer
a := Address{"Washington", "USA"}
//d = a // 这是不合法的,会报错:Address does not implement Describer
d = &a // 这是合法的,Address 类型的指针实现了 Describer 接口
d.Describe()
接口可以嵌套
类似于匿名结构体的嵌套 一个结构体实现了 A,B,那就说它也实现了 C
type A interface {
foo()
}
type B interface {
bar() int
}
type C interface {
A
B
}
接口的零值
接口的零值是 nil,同时其底层值(Underlying Value)和具体类型(Concrete Type)都为 nil。调用方法会 panic
接口的坑
- 不能把 interface 赋值为别的类型
func main() { // 声明 a 变量,类型 int,初始值为 1 var a int = 1 // 声明 i 变量,类型为 interface{}, 初始值为 a,此时 i 的值变为 1 var i interface{} = a // 声明 b 变量,尝试赋值 i var b int = i } - 切片也不能再分
func main() { sli := []int{2, 3, 5, 7, 11, 13} var i interface{} i = sli g := i[1:3] fmt.Println(g) }
三、实践练习例子:
今天以学习基础语法为主, 青训营 Go 视频讲的很好, 但是还有很多细节需要实践才能发现
四、课后个人总结:
Go 语言虽然简单,但是依然有许多小细节需要注意