Go语言上手-基础语言 | 青训营笔记

109 阅读7分钟

「Go 语言上手-基础语言」

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

1. 简介

1.1 什么是Go语言?

Go语言特点

  1. 高性能、高并发
  2. 语法简单、学习曲线平缓
  3. 丰富的标准库
  4. 完善的工具链
  5. 静态链接
  6. 快速编译
  7. 跨平台
  8. 垃圾回收

示例:实现一个简单的http服务器(承载静态文件访问,支持高性能高并发)

 package main
 ​
 import(
     "net/http"
 )
 ​
 func main(){
     //在标准库的http包里内建了路由,把"/"指向一个静态文件,实现对静态文件的访问
     http.Handle("/",http.FileServer(http.Dir(".")))
     //监听8080端口,并启动服务器
     http.ListenAndServe(":8080",nil)
 }

两行代码就实现了服务器端访问静态页面的任务,高性能

2 Go入门

2.1 开发环境

安装Go

官网链接:go.dev/

配置集成开发环境(VS code、Goland)

基于云的开发环境

链接:gitpod.io/#github.com…

可在线打开课程的示例项目,开始编码

2.2 基础语法

HelloWorld示例实现

 //程序的入口包
 package main
 //导入标准库里的fmt包,向屏幕输入输出字符串,格式化字符串
 import(
     "fmt"
 )
 ​
 func main(){
     //调用函数
     fmt.Println("hello world")
 }

变量

  • golang是一门强类型语言

  • 常见:字符串、整数、浮点数、布尔型

  • 变量两种声明方式:

    • var 变量名 = 变量值,此时主动推断变量类型
    • 变量名 := 变量值
  • 常量声明: const 常量名 = 常量值,同样根据上下文自动确定类型

 package main
 ​
 import(
     "fmt"
     "math"
 )
 ​
 func main(){
     //变量声明的两种方式:
     //1. var 变量名 = 变量值,此时主动推断变量类型
     var a = "initial"
     //也可以显式地写出变量类型
     var b,c int = 1,2
     var d = true
     var e = float64
     //2. 变量名 := 变量值
     f := float32(e)
     g := a + "foo"
     
     fmt.Println(a,b,c,d,e,f)//initial 1 2 true 0 0
     fmt.Println(g)          //initialapple
     
     //常量声明
     //const 常量名 = 常量值,同样可以根据上下文自动确定类型
     const s string = "constant"
     const h = 500000000
     const i = 3e20 / h
     fmt.Println(s,h,i,math.Sin(h),math.Sin(i))
 }

分支结构 if else

 package main
 ​
 import "fmt"
 ​
 func main(){
     //if后不跟(),且不可省略{},if后的语句不能写到同一行
     if 7%2 == 0 {
         fmt.Println("7 is even")
     }else{
         fmt.Println("7 is odd")
     }
     
     if 8%4 == 0 {
         fmt.Println("8 is divisible by 4")
     }
     
     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

  • 无需加break,自动跳出分支
  • case后支持多种表达形式,可加条件分支语句,避免if-else多层嵌套
 package main
 ​
 import (
     "fmt"
     "time"
 )
 ​
 func main(){
     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")
     }
     
     t := time.Now()
     //可以在case后面跟条件语句
     switch {
         case t.Hour() < 12:
             fmt.Println("It's before noon")
         default:
             fmt.Println("It's after noon")
     }  
 }

循环结构 for

  • 有且只有for循环
 package main
 ​
 import "fmt"
 ​
 func main(){
     i := 1
     for{
         fmt.Println("loop")
         break
     }
     for j := 7; j < 9; j++ {
         fmt.Println(j)
     }
     
     for n := 0; n < 5; n++ {
         if n%2 == 0 {
             continue
         }
         fmt.Println(n)
     }
     
     for i <= 3 {
         fmt.Println(i)
         i = i + 1
     }
 }

数组

  • 一维数组两种声明方式

    • var 数组名 [长度]类型,可以不赋初值
    • 数组名 := [长度]类型{数组元素值},要赋初值
 package main
 ​
 import "fmt"
 ​
 func main(){
 ​
     //一维数组两种声明方式
     //1. var 数组名 [长度]类型
     var a [5]int 
     a[4] = 100
     fmt.Println(a[4], len(a))
     //2. 数组名 := [长度]类型{数组元素值}
     b := [5]int{1,2,3,4,5}
     fmt.Println(b)
     
     var twoD [2][3]int
     for i := 0; i < 2; i++ {
         for j := 0; j < 3; j++ {
             twoD[i][j] = i + j
         }
     }
     fmt.Println("2d: ",twoD)
 }

切片(可变长度数组)

  • 切片的声明方式:

    • 切片名 := make([]元素类型,切片初始长度)
    • 切片名 := []元素类型{元素值}
  • 可以像数组一样,通过访问下标直接访问,并赋值

  • 通过append()方法动态添加,注意:必须将添加元素后的新切片重新赋值回原切片

  • copy()方法,实现克隆

  • s[start:end]、s[:end]、s[atart:]表达式,取出指定范围内元素

 package main
 ​
 import "fmt"
 ​
 func main(){
 ​
     //1.切片的声明方式:
     //  -----切片名 := make([]元素类型,切片初始长度)
     s := make([]string, 3)
     //  -----切片名 := []元素类型{元素值}
     good := []string{"g","o","o","d"}
     fmt.Println(good) //[g o o d]
     
     //2.可以通过访问下标直接赋值
     s[0] = "a"
     s[1] = "b"
     s[2] = "c"
     fmt.Println("get:", s[2]) // c
     fmt.Println("len:", len(s)) // 3
     
     
     //3. 通过append()方法动态填充,注意:必须将添加元素后的新切片重新赋值回原切片
     s = append(s, "d")
     s = append(s, "e", "f")
     fmt.Println(s)//[a b c d e f]
     
     c := make([]string, len(s))
     //4. copy()方法实现克隆
     copy(c,s)
     fmt.Println(c) //[a b c d e f]
     
     //5. 取出指定范围内元素
     fmt.Println(s[2:5])//[c d e]
     fmt.Println(s[:5]) //[a b c d e]
     fmt.Println(s[2:]) //[c d e f]
 }

map

  • map的声明方式

    • map名 := make(map[key类型]value类型)
    • map名 := map[key类型]value类型{k1:v1, k2:v2}
    • var map名 = map[key类型]value类型{k1:v1, k2:v2}
  • 赋值类似数组,通过[]访问key值直接赋值,访问不存在key对应的value,返回初始值0

  • r, ok := map名[key] 查询当前key是否存在

  • delete(map名,key)方法删除key-value对

package main

import "fmt"

func main(){

    //map的声明方式
    //1. map名 := make(map[key类型]value类型)
    //2. map名 := map[key类型]value类型{k1:v1, k2:v2}
    //3. var map名 = map[key类型]value类型{k1:v1, k2:v2}
    m := make(map[string]int)
    m2 := map[string]int{"one":1,"two":2}
    var m3 = map[string]int{"one":1,"two":2}
    fmt.Println(m2,m3)
    
    //赋值类似数组,通过访问key值直接赋值
    m["one"] = 1
    m["two"] = 2
    fmt.Println(m)         //map[one:1 two:2]
    fmt.Println(len(m))    //2
    fmt.Println(m["one"])  //1
    fmt.Println(m["three"])//0
   
    //r, ok := map名[key] 查询当前key是否存在
    r, ok := m["unknow"]
    fmt.Println(r,ok) //0 false
    
    delete(m,"one")  
}

range

  • 用于进行快速遍历,类似迭代器

  • 遍历数组,默认会返回两个值:下标和元素值

    遍历map,默认返回两个值:key和value;若只返回一个,返回key

package main

import "fmt"

func main(){

    nums := []int{2,3,4}
    sum := 0
    //对于数组会返回两个值:下标和元素值
    for i,num := range nums {
        sum += num
        if num == 2 {
            fmt.Println("index",i,"num:",num)//index:0 num:2
        }
    }
    fmt.Println(sum)//9
    
    m := map[string]string{"a":"A","b":"B"}
    //对于map会返回两个值:key和value
    //只返回一个时,返回key
    for k,v := range m {
        fmt.Println(k,v) //b B; a A
    }
    
    for k := range m {
        fmt.Println("key",k) //key a; key b
    }
}

函数

  • func 函数名(形参名 形参类型,形参名 形参类型) 返回值类型{}
  • 形参及返回值变量,类型后置

  • 一般函数都返回两个值,一是真正的函数运行结果,二是错误信息

  • 多个返回值用() 括起来,有变量名

指针

  • 用于对传入函数的参数值进行修改,默认是值传递机制,要想在函数里面修改外部变量,要创建一个指针指向原变量,将指针传入函数即可

  • 带指针的形式传入参数就能对原参数进行修改

  • 外部调用函数 &: add2ptr(&n)

    被调用函数内部 : func add2ptr(n *int) {}

结构体、结构体方法

  • 带类型的字段的集合,类似对象

  • 结构体声明

    type 结构体名 struct{字段名 字段类型 字段名 字段类型}

  • 作为方法函数的参数传入时,有值传入和指针两种方式

  • 结构体函数声明:

    func (结构体实例名 结构体名) 函数名 (参数 参数类型) 返回值类型{}

  • 结构体方法 (类成员函数) 可以直接由结构体实例名.方法名进行调用,相当于java类内部的方法

错误处理

  • 采用单独的返回值来传递错误信息,能清楚地知道是哪个函数发生了错误,并使用简单的 if-else 进行处理
  • 无错返回nil

字符串操作、格式化

  • fmt.Printf("p=%v\n",p)打印结构体字段的值
  • fmt.Printf("p=%+v\n",p)打印结构体的字段名称和值
  • fmt.Printf("p=%#v\n",p)打印结构体的包名.结构体名、字段名称和值

JSON处理

  • 保证结构体字段的首字母大写,则此结构体可用buf,err := json.Marshal(a)序列化,再使用string(buf)即可显示成json形式字符串,后续使用json.Unmarshal()反序列化

时间处理

  • 获取当前时间:time.Now()
  • 时间的格式化
  • 获取时间戳:t3,err := time.Parse("2006-01-02 15:04:05","2022-03-27 01:25:36) 其中,前一个基准时间是固定的

数字解析(数字 —— 字符串)

  • strconv.ParseFloat("1.234",64),可在参数中指明进制
  • strconv.Atoi("123"),自动转换,输入非数字字符串会报错

获取进程信息

  • os.Args进程的命令行参数

3 实战演练

3.1 猜谜游戏

游戏开始,玩家每次输入一个数字,系统判断是大于小于还是猜中,循环往复,直到猜中

3.1.1 生成随机数(0-100)

version-1
package main

import (
	"fmt"
	"math/rand"
)

func main() {
	maxNum := 100
	secretNumber := rand.Intn(maxNum)
	fmt.Println("The secret number is ", secretNumber)
}
  • 问题: 发现每次输出的随机数都是一样的
  • 解决: 设置随机数种子,获取程序每次启动的时间戳来初始化随机数种子
verson-2
func main() {
	maxNum := 100
    //初始化随机数种子
	rand.Seed(time.Now().UnixNano())
	secretNumber := rand.Intn(maxNum)
	fmt.Println("The secret number is ", secretNumber)
}

3.1.2 读取用户输入

  • 直接用os.Stdin不好处理,一般会用bufio.NewReader()将其转换为只读的流,对流进行处理
  • reader.ReadString("\n")从流中读取一行,注意: 会多读入一个换行符,需要去除
  • strconv.Atoi(input)实现字符串转数字 —— 报错!!!!
  • windows 系统和 Linux 系统的换行符不同,win10平台在按回车之后,会在参数的末尾加 \r\n 作为换行符,而代码只处理了\n,没有考虑\r\n

3.1.3 实现判断逻辑

version-4
  • 玩家只能输入一次猜测
  • 套循环

3.1.4 实现游戏循环

version-5
  • 输入有误 continue,输入类型有误 continue ,猜测错误 继续循环,猜测正确 跳出循环

3.2 在线词典

一个命令行词典,可以在命令行进行调用,去查询一个单词,输出单词的音标及注释。

  • 实现途径:调动第三方api进行查询,并输出
  • 学习目的:学习如何用go语言发送http请求,解析json,如何使用代码生成来提高效率

3.2.1 抓包

  • 打开第三方搜索网页(要求其响应体结构必须为json表达式):fanyi.caiyunapp.com/,输入单词进行查询,调出检查工具

  • 在发送的dict请求里找POST请求,并对该请求 —> 右键 —> copy —> copy as cURL

curl 'https://api.interpreter.caiyunai.com/v1/dict' \
  -H 'Accept: application/json, text/plain, */*' \
  -H 'Accept-Language: zh-CN,zh;q=0.9' \
  -H 'Cache-Control: no-cache' \
  -H 'Connection: keep-alive' \
  -H 'Content-Type: application/json;charset=UTF-8' \
  -H 'Origin: https://fanyi.caiyunapp.com' \
  -H 'Pragma: no-cache' \
  -H 'Referer: https://fanyi.caiyunapp.com/' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: cross-site' \
  -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36' \
  -H 'X-Authorization: token:qgemv4jr1y38jyq6vhvi' \
  -H 'app-name: xy' \
  -H 'device-id: ' \
  -H 'os-type: web' \
  -H 'os-version: ' \
  -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  --data-raw '{"trans_type":"en2zh","source":"good"}' \
  --compressed

3.2.2 代码生成

  • 代码生成网页curlconverter.com/#go

  • 此时输入固定,要实现用户输入 ----- json序列化

3.2.3 生成 request body

  • 构造一个请求结构体DictRequest,结构体内部字段名称与json字符串一一对应,然后直接调用json.Marshal()方法即可实现

3.2.4 解析 response body

  • 一般方法,定义结构体,其内字段与json字符串一一对应即可反序列化

  • 采用代码生成工具oktools.net/json2go,将浏览器检查工具里的请求的preview中的json字符串转化为结构体

  • 创建响应结构体DictResponse,反序列化返回的json字符串

3.2.5 打印结果

  • 选取需要的字段进行打印
  • 注意:在接收到了响应之后,先检查响应状态码是否为200(成功)否则也不用往下了,并且开启日志便于调试

3.2.6 完善代码

  • 将main函数改为query函数,传入需要查询的word参数,word从命令行获取,并事先进行部分验证