《Go 语言上手-基础语言》总结:Go语言的基础语法 | 青训营笔记

416 阅读7分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第1篇笔记 写在前面
第一次观看字节青训营的课程,讲师还是非常有水准的,受益匪浅。不过有一些基础语法讲的速度有些快了,课上没有来得及记录完整的笔记,所以课下查阅一些文章做了二次整理。
我的主力语言是Java,之前久仰Go的大名,这是我第一次接触并学习Go语言,很多特性和底层原理暂时不了解,所以如果笔记中有些观点有误导嫌疑,麻烦指正,我会立即删除或修改。
由于只是基础语法部分的学习,笔记内容并不完善,我希望随着课程的深入学习,持续追加迭代一些内容,直到形成自己可用的完整知识体系。

正文部分

1、本堂课重点内容

  • Go的环境配置
  • Go的基础语法
  • 如何使用Go来发送HTTP请求

2、详细知识点介绍:GO的基础语法

1、概述

1、GO的特点

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

2、字节为什么转GO

  • 最早使用Python,由于性能问题转为GO
  • C++不适合在线Web业务
  • 早期团队非Java背景
  • GO性能好,部署简单
  • 内部研发了GO的RPC框架和HTTP框架

2、导包

1、做法

使用import引入其他的包,用小括号包裹:

import (
    "fmt"
)

不同的包之间换行就行,不用加分隔符

2、导包的逻辑

对于导入的包:

  • 编译器会首先在GOROOT中寻找
  • 随后会在项目所对应的GOPATH中寻找
  • 最后才是在全局GOPATH中寻找
  • 如果都无法找到,编译器将会报错

3、注意事项

和Java有所不同,在Golang中,import导入的是源文件的相对路径,而不是包名。

Golang没有强制要求包名和目录名需要一致。包名和路径其实是两个概念,Java淡化了这种思想。

在代码中引用包内的成员时,使用包名而不是目录名,通常的引用格式是packageName.FunctionName

3、声明变量

Golang是强类型的一种语言,所有的变量必须拥有类型,并且变量只可以存储特定类型的数据。

1、显式指定类型

在Golang中定义一个变量,需要使用var关键字,但是需要把变量的类型写在变量名后面:

var a int
var b float32
var c = "initial"

也允许一次性定义多个变量

var c, d float64

这种方式定义的变量也可以直接赋值,但是由于根据赋的值能够确认类型,所以可以省略掉变量类型:

image-20220507130301335

2、":=" 直接赋值

这种方式不需要显式指定变量的类型,也不需要写var

只需要声明变量名称即可,后面使用“:=”符号来赋值

e, f := 9, 10

3、匿名变量

标识符为“_”的变量,是系统保留的匿名变量。特点是在赋值后,会被立即释放,称为匿名变量。

匿名变量的作用是作为变量的占位符,通常会在批量赋值时使用。

比如有个函数返回了3个值,只需要使用其中的一个,其他两个就可以使用匿名变量来接收,避免为了不需要的数据去额外声明一些变量

func main() {
  // 调用函数,仅仅需要第二个返回值,第一,三使用匿名变量占位
  _, data, _ := getData()
  fmt.Println(data)
}
// 返回两个值的函数
func getData() (int, int, int) {
  // 返回3个值
  return 12, 3
}

4、声明常量

把var换成content即可,注意常量不能使用":="符号来赋值

const s string = "constant"
const h = 500000000
const i = 3e20 / h

常量可以显式指定数据类型,也可以不指定,根据赋值的实际类型来确认类型。

5、if 条件判断语句

if后面的条件不用加小括号,但是大括号不能省

if 7%2 == 0 {
    fmt.Println("7 is even")
} else {
    fmt.Println("7 is odd")
}

特殊之处在于,GO允许在if之前执行一条简单的语句,使用一个分号和条件语句分隔开。比如:

func main() {
    count := 0
    if count = 10; count > 5 {
        count = 20
    }
    fmt.Println(count) // 20
}

6、for 循环语句

Go中只有 for 循环一种循环,但也能实现while、do-while等循环的功能。

和 if 一样,for 循环的条件也不需要加小括号,大括号不能省

标准的for循环格式,也是三个部分使用分号隔开:

// for i循环
for i := 7; i < 9; i++ {
    fmt.Println(i)
}

三个部分都可以省略,就成了没有结束条件的死循环,相当于while(true) {}

// 死循环
for {
    fmt.Println("loop")
}

如果只保留循环的条件,就实现了while()循环

// 类似while循环
i := 1
for i <= 3 {
    fmt.Println(i)
    i = i + 1
}

7、switch 分支语句

GO中的switch,匹配到一个case就会停止,不会进入其他分支

a := 2
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的Switch支持很多种类型作为条件,Java支持的就比较少。

8、数组

声明一个数组,需要指定元素类型和数组长度

var a [5]int

之后就可以正常操作数组的元素:

a[4] = 100
fmt.Println(a[2])

这个数组也是不可变大小的。

9、切片 slice

1、格式

相当于一个可变长度的数组,但切片并不存储任何数据,它只是描述了底层数组中的一段。

思想是,把一个数组按照需要的长度去使用。

切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔,包括上界,但不包括下界。

a[1:4]

上面代码的含义是创建一个切片,它包含 a 中下标从 1 到 3 的元素

2、底层原理

切片不是拷贝了一个新数组,而是定义了新的指针,指向了原来数组所在的内存空间。

所以,修改了切片数组的值,也就相应的修改了原数组的值了。

切片可以用append增加元素。但是如果此时底层数组容量不够,此时切片将会指向一个重新分配空间后进行拷贝的数组。

如果在一个数组上创建了多个切片,每个切片都可以对数组的实际数据进行修改,进而影响到其他切片的元素值。

3、make

可以使用make来创建切片,make是一个内置函数。

创建一个长度为5的数组,并返回一个引用了它的切片:

a := make([]int, 5)

上面创建的数组,容量和长度都为5。也可以去分别指定切片的长度和容量:

b := make([]int, 0, 5)

切片长度和数组长度的区别:

  • 数组长度是切片使用的底层数组的实际长度,也可以称为切片容量,因为它容纳了切片。
  • 切片长度是切片声明的长度

10、map

1、声明与初始化

声明一个map:

var m map[string]string

一个空的map,取任何值都是返回对应类型的初始值

map在声明后,必须经过初始化才能使用:

var m map[string]string
m = make(map[string]int)

也可以在声明时直接赋值:

m := make(map[string]int)

2、键值的限制

Go中,map的key必须是可以比较的类型:

image-20220507140752406

Value可以是任意类型。

注意,golang为uint32、uint64、string提供了fast access,使用这些类型作为key可以提高map访问速度

3、使用方式

// 新增
m["key"] = "value"
​
// 删除,key不存在则不作操作
delete(m, "key")
​
// 更新
m["key"] = "value2"
​
// 查询,key不存在就返回value类型的零值,ok指的是元素是否存在
i := m["key"]
i, ok := m["key"]
_, ok := m["key"]

11、range

用于遍历数组或者集合,比如map

1、遍历数组

func main() {
    a :=[5]int{1,2,3,4,5}
    for index, value := range a{
        fmt.Println(index,value)
    }
}

类似Java中的for-each,可以声明两个变量:

  • index:索引
  • value:当前访问的值

如果不需要索引或者值,可以使用匿名变量

2、遍历map集合

func main() {
    a := map[string]string{"key":"key1","value":"value1"}
    for k, v := range a{
        fmt.Println(k,v)
    }
}

12、定义函数

所有的函数都以func开头,后面跟方法名和参数列表,最后面是返回值。

例如:

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

1、函数名

函数名有讲究:

  • 如果首字母是小写,则只能在包内使用,相当于private
  • 如果首字母是大写,则可以在包外被引入使用,相当于public

2、参数列表

一个GO函数可以不接收参数,也可以接收多个参数。

在声明参数列表时,形参名在前,参数类型在后:

func add(y int) int {
}

3、返回值

一个GO函数可以返回多个返回值,一般是一个是实际返回值,另一个用于异常处理

由于可以存在多个返回值,所以返回值是可以命名的,返回时就会按照命名的赋值去返回。

func split(sum int) (x, y int) {
    x = sum
    y = 2
    return
}

同样,在接收方法的返回值时,如果方法有有多个返回值,就这样接收:

a, b := split(10)

也可以使用匿名变量。

13、defer关键字

1、格式

defer 语句会将函数推迟到外层函数返回之后执行。 注意defer后面必须是函数调用语句,不能是其他语句,否则编译器会报错

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

举个例子:

func main() {
    defer fmt.Println("a")
​
    fmt.Println("b")
}

本来按照顺序执行,应该打印出a、b,但是由于defer语句推迟了第一行代码的执行,所以实际输出是b、a

2、使用场景

其实非常好用,比如传统打开一个资源是这样的:

// 打开文件
open()
// 处理文件
do somthing...
do somthing...
do somthing...
// 关闭文件
close()

使用defer关键字之后,就能把资源的申请与释放的代码写在一起,有两个好处:

  • 更简洁
  • 避免忘记关闭资源造成OOM
// 打开文件
open()
// 关闭文件
defer close()
// 处理文件
do somthing...
do somthing...
do somthing...

3、特点

如果定义了多条defer语句,执行顺序是:

  • 按照代码书写顺序,先顺序执行不带defer的
  • 之后从下往上依次执行带defer的
func main() {
    defer fmt.Println("a")
    fmt.Println("x")
    defer fmt.Println("b")
    defer fmt.Println("c")
}

输出:

image-20220507133139121

14、指针

用&取地址,用*取地址中的值。

和C不同,GO没有指针运算。

比如这个程序:

func main() {
    n := 5
    add(n)
    fmt.Println(n) // 5
}
​
func add(n int) {
    n += 2
}

由于方法中操作的n其实是传入的n的一个拷贝,所以不会改变原先n的值

想要实现方法直接修改实参的值,就需要使用指针:

func main() {
    n := 5
    // 取n的地址,交给方法
    add(&n)
    fmt.Println(n) // 7
}
​
func add(n int) {
    // 操作地址的值
    *n += 2
}

3、实践练习例子

1、猜谜游戏注释版

只是对讲师课上讲的代码进行简单修改和注释,不涉及作业内容

package main

import (
   "bufio"
   "fmt"
   "math/rand"
   "os"
   "strconv"
   "strings"
   "time"
)

func main() {
   maxNum := 100
   rand.Seed(time.Now().UnixNano())
   secretNum := rand.Intn(maxNum)
   // fmt.Println("答案是: ", secretNum)

   fmt.Printf("请输入一个数字:")
   // 只读的流
   reader := bufio.NewReader(os.Stdin)
   // 用户可以多次输入,直到猜对
   for {
      // 读取一行
      input, err := reader.ReadString('\n')
      if err != nil {
         fmt.Println("输入错误")
         continue
      }
      // 去掉这一行的换行符
      input = strings.TrimSuffix(input, "\n")
      // 把字符串转为数字
      guess, err := strconv.Atoi(input)
      if err != nil {
         fmt.Println("数据转换错误")
         continue
      }
      fmt.Printf("你输入的是: %v ", guess)

      // 比较用户输入和随机数的大小
      if secretNum > guess {
         fmt.Println("小了,继续猜")
      } else if secretNum < guess {
         fmt.Println("大了,继续猜")
      } else {
         fmt.Println("你猜对了,答案是: ", guess)
         // 猜对了就终止循环
         break
      }
   }

}

2、翻译工具注释版

只是对讲师课上讲的代码进行简单修改和注释,不涉及作业内容
讲师提供的版本是需要在运行时传入单词,我觉得有些麻烦,改成了输入一个单词之后调用方法进行查询的形式,没有本质区别

package main

import (
   "bufio"
   "bytes"
   "encoding/json"
   "fmt"
   "io/ioutil"
   "log"
   "net/http"
   "os"
   "strings"
)

// 构造请求结构体,用于转成JSON
type DictRequest struct {
   TransType string `json:"trans_type"`
   Source    string `json:"source"`
   UserID    string `json:"user_id"`
}

// 构造响应结构体
type DictResponse struct {
   Rc   int `json:"rc"`
   Wiki struct {
      KnownInLaguages int `json:"known_in_laguages"`
      Description     struct {
         Source string      `json:"source"`
         Target interface{} `json:"target"`
      } `json:"description"`
      ID   string `json:"id"`
      Item struct {
         Source string `json:"source"`
         Target string `json:"target"`
      } `json:"item"`
      ImageURL  string `json:"image_url"`
      IsSubject string `json:"is_subject"`
      Sitelink  string `json:"sitelink"`
   } `json:"wiki"`
   Dictionary struct {
      Prons struct {
         EnUs string `json:"en-us"`
         En   string `json:"en"`
      } `json:"prons"`
      Explanations []string      `json:"explanations"`
      Synonym      []string      `json:"synonym"`
      Antonym      []interface{} `json:"antonym"`
      WqxExample   [][]string    `json:"wqx_example"`
      Entry        string        `json:"entry"`
      Type         string        `json:"type"`
      Related      []interface{} `json:"related"`
      Source       string        `json:"source"`
   } `json:"dictionary"`
}

func main() {
   fmt.Printf("请输入一个单词:")
   // 只读的流
   reader := bufio.NewReader(os.Stdin)
   input, err := reader.ReadString('\n')
   if err != nil {
      fmt.Println("输入错误")
   }
   // 去掉这一行的换行符
   input = strings.TrimSuffix(input, "\n")
   query(input)
}

// 查询一个单词的音标和示例
func query(word string) {
   client := &http.Client{}
   // 构造请求数据
   // var data = strings.NewReader(`{"trans_type":"en2zh","source":"hello"}`)
   request := DictRequest{TransType: "en2zh", Source: word}
   buf, err := json.Marshal(request)
   if err != nil {
      log.Fatal(err)
   }
   // 把字符数组转换为字符串
   var data = bytes.NewReader(buf)

   // 创建请求(流式创建,减小内存占用)
   req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
   if err != nil {
      log.Fatal(err)
   }
   // 设置请求头
   req.Header.Set("Accept", "application/json, text/plain, */*")
   req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
   req.Header.Set("Connection", "keep-alive")
   req.Header.Set("Content-Type", "application/json;charset=UTF-8")
   req.Header.Set("Origin", "https://fanyi.caiyunapp.com")
   req.Header.Set("Referer", "https://fanyi.caiyunapp.com/")
   req.Header.Set("Sec-Fetch-Dest", "empty")
   req.Header.Set("Sec-Fetch-Mode", "cors")
   req.Header.Set("Sec-Fetch-Site", "cross-site")
   req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36")
   req.Header.Set("X-Authorization", "token:qgemv4jr1y38jyq6vhvi")
   req.Header.Set("app-name", "xy")
   req.Header.Set("os-type", "web")
   req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"`)
   req.Header.Set("sec-ch-ua-mobile", "?0")
   req.Header.Set("sec-ch-ua-platform", `"Windows"`)
   // 发起请求
   resp, err := client.Do(req)
   if err != nil {
      log.Fatal(err)
   }
   // 关闭流
   defer resp.Body.Close()
   // 读取响应,把流读入内存
   bodyText, err := ioutil.ReadAll(resp.Body)
   if err != nil {
      log.Fatal(err)
   }
   // 判断HTTP响应状态码
   if resp.StatusCode != 200 {
      log.Fatal("bad StatusCode:", resp.StatusCode)
   }

   // fmt.Printf("%s\n", bodyText)
   // 解析响应结果
   var dictResponse DictResponse
   // 把响应的JSON封装成DictResponse结构体
   err = json.Unmarshal(bodyText, &dictResponse)
   if err != nil {
      log.Fatal(err)
   }
   // fmt.Printf("%#v\n", dictResponse)
   // 输出单词的音标
   fmt.Println(word, "UK", dictResponse.Dictionary.Prons.En, "US", dictResponse.Dictionary.Prons.EnUs)
   // 循环输出单词的示例
   for _, item := range dictResponse.Dictionary.Explanations {
      fmt.Println(item)
   }

}

4、课后个人总结

1、课程中涉及到的工具网站

2、课程中遇到的问题

  • Windows默认不支持nc指令,解决方法可以看这篇帖子:www.cnblogs.com/linyufeng/p… 注意解压之前需要关闭杀毒软件,否则会判定为黑客软件惨遭删除

5、引用参考