Go 语言语法基础 | 青训营笔记

96 阅读8分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

一、本堂课重点内容

  • 熟练使用切片

  • Map 无序

  • Range 遍历

  • 极为有限的指针(变量, 函数, 方法)

  • 错误处理

  • 字符串处理库 strings fmt %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 语言虽然简单,但是依然有许多小细节需要注意

五、引用参考: