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

97 阅读2分钟

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

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

知识要点

  • 什么是Go语言

    • Go语言基本特征
    • Go语言应用优势
  • Go语言入门

    • 安装开发环境
    • Go语言基础语法

重点:Go语言基础语法

包、变量、函数

每个 Go 程序都是由包构成的。

程序从 main 包开始运行。

按照约定,包名与导入路径的最后一个元素一致。例如,"math/rand" 包中的源码均以 package rand 语句开始。

导入与导出

 import (
     "fmt"
     "math"
 )

此代码用圆括号组合了导入,这是 “分组” 形式的导入语句。当然也可以编写多个导入语句,不过使用分组导入语句是更好的形式。

在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。在导入一个包时,你只能引用其中已导出的名字。任何 “未导出” 的名字在该包外均无法访问。

例如:你只能使用math.Pi而非math.pi

函数

函数可以没有参数或接受多个参数。注意类型在变量名之后

 func add(x int, y int) int {
     return x + y;
 }

当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。

 func add(x, y int) int {
     return x + y;
 }

函数可以返回任意数量的返回值。

 func swap(x, y int) (int, int) {
     return y, x;
 }

变量

var 语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后。var 语句可以出现在包或函数级别。

 var a, b, c int

变量声明可以包含初始值,每个变量对应一个。如果初始化值已存在,则可以省略类型;变量会从初始值中获得类型。

 var i, j int = 1, 2

短变量声明。在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。

函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。

 func main() {
     var a, b, c int = 1, 2, 3
     c, python, java := "no", "no", "no"
 }

在声明一个变量而不指定其类型时(即使用不带类型的 := 语法或 var = 表达式语法),变量的类型由右值推导得出。

 v := 42
 fmt.Printf("v is of type %T\n", v) //输出v is of type int

常量

常量的声明与变量类似,只不过是把var关键字改成 const 关键字。

常量可以是字符、字符串、布尔值或数值。

常量不能用 := 语法声明。

 const s string = "constant"
 const World = "world"
 const h = 50000
 const i = 3e18 / h

循环结构

Go 只有一种循环结构:for 循环。

 for { //死循环
     
 }
 ​
 for i := 1; i < 10; i++ {
     fmt.Println(i)
 }
 ​
 for i := 1; i < 10; i++ {
     if i % 2 == 0 {
         continue
     }
 }
 ​
 i = 0
 for i <= 3 { //while循环的写法
     fmt.Println(i)
     i = i + 1
 }

注意:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { } 则是必须的。

if-else

Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。

for 一样, if 语句可以在条件表达式前执行一个简单的语句。

该语句声明的变量作用域仅在 if 之内。

 if num := 9; num < 0 {
     fmt.Println(num, "is negative")
 } else if num < 10 {
     fmt.Println(num, "has 1 digit")
 } else {
     fmt.Println(num, "has multiple digits")
 }

switch

switch 是编写一连串 if - else 语句的简便方法。它运行第一个值等于条件表达式的 case 语句。

Go 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的,不过 Go 只运行选定的 case,而非之后所有的 case。 实际上,Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。 Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。

 a := 2
 switch a {
 case 1:
     fmt.Println("one")
 case 2:
     fmt.Println("two")
 case 3, 4:
     fmt.Println("three or four")
 default:
     fmt.Println("other")
 }
 t := time.Now()
 switch {
 case t.Hour() < 12:
     fmt.Println("morning")
 default:
     fmt.Println("afternoon")
 }

defer栈

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

 func main() {
     fmt.Println("counting")
 ​
     for i := 0; i < 10; i++ {
         defer fmt.Println(i)
     }
 ​
     fmt.Println("done")
 }

运行结果:

 counting
 done
 9
 8
 7
 6
 5
 4
 3
 2
 1
 0

指针

类型 *T 是指向 T 类型值的指针。其零值为 nil

 var p *int

& 操作符会生成一个指向其操作数的指针。

 i := 42
 p = &i

* 操作符表示指针指向的底层值。

 fmt.Println(*p) // 通过指针 p 读取 i
 *p = 21         // 通过指针 p 设置 i

这也就是通常所说的 “间接引用” 或 “重定向”。

与 C 不同,Go 没有指针运算。也就是说,指针的一个主要用途就是对于传入参数进行修改。

 func main() {
     i, j := 42, 2701
 ​
     p := &i         // 指向 i
     fmt.Println(*p) // 通过指针读取 i 的值
     *p = 21         // 通过指针设置 i 的值
     fmt.Println(i)  // 查看 i 的值
 ​
     p = &j         // 指向 j
     *p = *p / 37   // 通过指针对 j 进行除法运算
     fmt.Println(j) // 查看 j 的值
 }

结构体

 type Vertex struct {
     X, Y int
 }
 ​
 func main() {
     v := Vertex{1, 2}
     v.X = 4
     fmt.Println(v.X)
 }

创建结构体:

 var (
     v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
     v2 = Vertex{X: 1}  // Y:0 被隐式地赋予
     v3 = Vertex{}      // X:0 Y:0
     p  = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
 )

切片

每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。

类型 []T 表示一个元素类型为 T 的切片。

切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:

 a[low : high]

它会选择一个半开区间,包括第一个元素,但排除最后一个元素。

以下表达式创建了一个切片,它包含 a 中下标从 1 到 3 的元素:

 a[1:4]

实例:

 func main() {
     primes := [6]int{2, 3, 5, 7, 11, 13}
     var s []int = primes[1:4]
     fmt.Println(s)
 }

切片就像数组的引用

切片并不存储任何数据,它只是描述了底层数组中的一段。更改切片的元素会修改其底层数组中对应的元素。与它共享底层数组的切片都会观测到这些修改。

切片的零值是 nil。nil 切片的长度和容量为 0 且没有底层数组。

用make创建切片

切片可以用内建函数 make 来创建,这也是你创建动态数组的方式。

make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:

 a := make([]int, 5)  // len(a)=5

要指定它的容量,需向 make 传入第三个参数:

 b := make([]int, 0, 5) // len(b)=0, cap(b)=5
 ​
 b = b[:cap(b)] // len(b)=5, cap(b)=5
 b = b[1:]      // len(b)=4, cap(b)=4

示例:

 func main() {
     a := make([]int, 5)
     printSlice("a", a) //a len=5 cap=5 [0 0 0 0 0]
 ​
     b := make([]int, 0, 5)
     printSlice("b", b) //b len=0 cap=5 []
 ​
     c := b[:2]
     printSlice("c", c) //c len=2 cap=5 [0 0]
 ​
     d := c[2:5]
     printSlice("d", d) //d len=3 cap=3 [0 0 0]
 }
 ​
 func printSlice(s string, x []int) {
     fmt.Printf("%s len=%d cap=%d %v\n",
         s, len(x), cap(x), x)
 }

向切片追加元素

为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 append 函数。

 func main() {
     var s []int
     printSlice(s)
 ​
     // 添加一个空切片
     s = append(s, 0)
     printSlice(s)
 ​
     // 这个切片会按需增长
     s = append(s, 1)
     printSlice(s)
 ​
     // 可以一次性添加多个元素
     s = append(s, 2, 3, 4)
     printSlice(s)
 }

Range

for 循环的 range 形式可遍历切片或映射。

当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。

 func main() {
     nums := []int{2, 3, 4}
     sum := 0
     for i, num := range nums {
         sum += num
         if num == 2 {
             fmt.Println("index: ", i)
         }
     }
     fmt.Println(sum)
 ​
     m := map[string]string{"a": "A", "b": "B"}
     for k, v := range m {
         fmt.Println(k, v)
     }
 }

可以将下标或值赋予 _ 来忽略它。

 for i, _ := range pow
 for _, value := range pow

若你只需要索引,忽略第二个变量即可。

 for i := range pow

映射

映射将键映射到值。

映射的零值为 nilnil 映射既没有键,也不能添加键。

make 函数会返回给定类型的映射,并将其初始化备用。

 func main() {
     m = make(map[string]Vertex)
     m["Bell Labs"] = Vertex{
         40.68433, -74.39967,
     }
     fmt.Println(m["Bell Labs"])
 }

在映射 m 中插入或修改元素:

 m[key] = elem

获取元素:

 elem = m[key]

删除元素:

 delete(m, key)

通过双赋值检测某个键是否存在:

 elem, ok = m[key]

keym 中,oktrue ;否则,okfalse

key 不在映射中,那么 elem 是该映射元素类型的零值。

错误处理

不同于异常,个人觉得go中的错误处理更加通俗易懂,和ifelse没什么两样。

在函数里面,我们可以在哪个函数的返回值类型里面,后面加一个error,就代表这个函数可能会返回错误。

那么在函数实现的时候,需要同时return两个值,要么就是如果出现错误的话,可以return nil和一个error。如果没有的话,就返回原本的结果和nil。

 package main
 ​
 import "fmt"
 import "errors"
 ​
 type user struct {
     name     string
     password string
 }
 ​
 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", "1234"}}, "wag")
     if err != nil {
         fmt.Println(err)
         return
     }
     fmt.Println(u.name)
 }

JSON处理

go语言里面的JSON操作非常简单,对于一个已有的结构体,只要保证每个字段的第一个字母是大写。那么这个结构体就能用JSON.marshaler去序列化变成一个JSON的字符串。

序列化之后的字符串也能够用JSON.unmarshaler去反序列化到一个空的变量里面。

 package main
 ​
 import (
     "encoding/json"
     "fmt"
 )
 ​
 type user struct {
     Name  string
     Age   int `json:"age"`
     Hobby []string
 }
 ​
 func main() {
     a := user{Name: "wang", Age: 18, Hobby: []string{"c", "python", "java"}}
     buf, err := json.Marshal(a)
     if err != nil {
         panic(err)
     }
     fmt.Println(string(buf)) //{JSON字符串}
 ​
     var b user
     err = json.Unmarshal(buf, &b)
     if err != nil {
         panic(err)
     }
     fmt.Println("%#v\n", b) // {wang 18 [c python java]}
 ​
 }

数字与字符串转化

关于字符串和数字类型之间的转换都在strconv这个包下。

parseInt或者parseFloat来解析一个字符串,用Atoi把一个十进制字符串转成数字,用itoA把数字转成字符串。

如果输入不合法,这些函数都会返回error。

 package main
 ​
 import (
     "fmt"
     "strconv"
 )
 ​
 func main() {
     f, _ := strconv.ParseFloat("1.234", 64)
     fmt.Println(f) //1.234
 ​
     n, _ := strconv.ParseInt("111", 10, 64)
     fmt.Println(n) //111
 ​
     n, _ = strconv.ParseInt("0x1000", 0, 64)
     fmt.Println(n) //4096
 ​
     n, _ = strconv.ParseInt("1000", 2, 64)
     fmt.Println(n) //8
 ​
     n2, _ := strconv.Atoi("123")
     fmt.Println(n2) //123
 ​
     n2, err := strconv.Atoi("AAA")
     fmt.Println(n2, err) //0 strconv.Atoi: parsing "AAA": invalid syntax
 }

引用参考

链接1

链接2