这是我参与「第三届青训营 -后端场」笔记创作活动的第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
这种方式定义的变量也可以直接赋值,但是由于根据赋的值能够确认类型,所以可以省略掉变量类型:
2、":=" 直接赋值
这种方式不需要显式指定变量的类型,也不需要写var
只需要声明变量名称即可,后面使用“:=”符号来赋值
e, f := 9, 10
3、匿名变量
标识符为“_”的变量,是系统保留的匿名变量。特点是在赋值后,会被立即释放,称为匿名变量。
匿名变量的作用是作为变量的占位符,通常会在批量赋值时使用。
比如有个函数返回了3个值,只需要使用其中的一个,其他两个就可以使用匿名变量来接收,避免为了不需要的数据去额外声明一些变量
func main() {
// 调用函数,仅仅需要第二个返回值,第一,三使用匿名变量占位
_, data, _ := getData()
fmt.Println(data)
}
// 返回两个值的函数
func getData() (int, int, int) {
// 返回3个值
return 1, 2, 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必须是可以比较的类型:
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")
}
输出:
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、课程中涉及到的工具网站
- oktools.net/ 用于将response转为Go的结构体
- curlconverter.com/#go 用于将curl转为go的结构体
2、课程中遇到的问题
- Windows默认不支持nc指令,解决方法可以看这篇帖子:www.cnblogs.com/linyufeng/p… 注意解压之前需要关闭杀毒软件,否则会判定为黑客软件惨遭删除