Go入门笔记&实训总结 | 青训营笔记

60 阅读12分钟

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

Go入门笔记&实训总结

一 、配置环境

如果只下载go以及vscode,发现无法查看源码等消息,因此我们需要接下来的四步操作:

  • 首先,按照官方下载go的安装包
  • 随后下载VScode编辑器
  • 修改电脑中的环境变量
  • 随后重启VScode,对bin再次进行更新下载

二、基础语法

2.1 语言结构

以hello world为例

package main
//package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
import "fmt"
//告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数
func main() {
   /* main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数) */
   fmt.Println("Hello, World!")//可以将字符串输出到控制台,并在最后自动增加换行字符 \n
}

在调用Println也可以发现这个函数的开头是大写字母

  • 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public)
  • 标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )

2.2 变量与常量

大部分与C++类,只不过go语言的字符串是内置类型,可以直接通过加号拼接,也能够直接用等于号去比较两个字符串。声明变量的方式:

    //多变量声明(同时赋值)
    var b, c int = 1, 2
    b, c = c, b //可以直接这样交换
    //根据值自行判定变量类型
    var d = true
    //指定变量类型,如果没有初始化,则变量默认为零值
    var e float64
    //:= 为赋值操作符,可以自动声明该变量的类型并完成初始化
    f := float32(e)
    g := a + "foo"
    _, b = 5, 7 //空白标识符 _ 也被用于抛弃值,如例子中5中被抛弃。//常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
    const s string = "constant"
    const h, j = 500000000, 34
    const i = 3e20 / h

【注意事项】

  1. 如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20 就是不被允许的,编译器会提示错误 no new variables on left side of :=,但是 a = 20 是可以的,因为这是给相同的变量赋予一个新的值。
  2. 如果是声明了一个局部变量,那么该变量必须被使用(全局变量是允许声明但不使用的),因此也引入了空白标识符_
  3. go里面没有隐式类型转换,想转必须强转,如设sumcount为整形,将其转化为浮点型mean = float32(sum)/float32(count)
  4. const可用于枚举】iota可以认为是一个可以被编译器修改的常量,iota 在const关键字出现时将被重置为 0(const 内部的第一行之前),const中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。iota 可以被用作枚举值:
func main() {
    const (
            a = iota   //0
            b          //1
            c          //2
            d = "ha"   //独立值,iota += 1
            e          //"ha"   iota += 1
            f = 100    //iota +=1
            g          //100  iota +=1
            h = iota   //7,恢复计数
            i          //8
    )

2.3 运算操作符

与C++一样,指针那里甚至比C++还要简单一些

2.4 条件判断

首先,Go 没有三目运算符,所以不支持 ?: 形式的条件判断。

if-else

与C++一样,但主要有两点不同:

  • 这里的if后面都是不带括号的!
  • 不同点是Golang里面的if,它必须后面接大括号,就是你不能像C或者C++一样,直接把if里面的语句同一行。
    if 7%2 == 0 {   //大括号也必须在同一行
        fmt.Println("7 is even")
    } else {
        fmt.Println("7 is odd")
    }

switch

这里有个很大的一点不同的是,在c++里面,switch case如果不不显示加break的话会然后会继续往下跑完所有的case,在go语言里面的话是不需要加 break的。

go语言里面的switch功能更强大。可以使用任意的变量类型,甚至可以用来取代任意的if else语句。你可以在switch后面不加任何的变量,然后在case里面返回条件分支。这样代码相比你用多个if else代码逻运会更为清晰。

    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("It's before noon")
    default:
        fmt.Println("It's after noon")
    }

2.5 循环判断

与C++一样,只不过没有括号,并且能转换成while的样式

//for样式 
for n := 0; n < 5; n++ {
        if n%2 == 0 {
            continue
        }
        fmt.Println(n)
    }
//while样式
    for i <= 3 {
        fmt.Println(i)
        i = i + 1
    }

2.6 数组

就是一个数组,但是业务中更多使用的是「切片」。

//以一个二维数组为例 
var twoD [2][3]int//声明数组
//初始化数组
for i := 0; i < 2; i++ {
    for j := 0; j < 3; j++ {
        twoD[i][j] = i + j
    }
}

如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度:

balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

2.7 切片

用法python里面的切片。但是使用时类似C++的vector

创造切片

s := make([]string, 3)//第一个声明类型,第二个是初始的切片长度,第三个可选参数[capacity]允许指定最大容量!

切片赋值

切片是可索引的,并且可以由 len() 方法获取长度。

一个切片在未初始化之前默认为 nil,长度为 0

//如同vector,已经申请的空间可以直接赋值
s[2] = "c"
good := []string{"g", "o", "o", "d"}
//若想在加,就要使用append进行扩容
s = append(s, "e", "f") //可以一下子加很多个元素
copy(c, s)//切片赋值

切片的使用

fmt.Println(s) // [a b c d e f]
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]

2.8 Map

Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是完全无序的,我们无法决定它的返回顺序!!

m := make(map[string]int)//map声明
m["two"] = 2//赋值
//map的声明&初始化的方法
m2 := map[string]int{"one": 1, "two": 2}

【删除map中的元素使用delete】

delete(m, "one")

【打印map--切记无序】

m := map[string]string{"a": "A", "b": "B"}
for k, v := range m {
    fmt.Println(k, v) // b 8; a A
}

2.9 range

range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。

在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。(因此是一直返回两个值的!!)

//索引&值
for i, num := range nums {
    sum += num
    if num == 2 {
        fmt.Println("index:", i, "num:", num) // index: 0 num: 2
    }
} 
//打印集合的例子在上面有

2.10 函数

基本遵循下面的结构:

格式:func 函数名( [参数列表] ) [返回值类型],需要注意的是变量类型是后置的

需要注意的是,go的函数和c++相似,其参数列表也分为

  • 值传递
  • 引用传递
//案例
func add(a int, b int) int {
    return a + b
}
//返回值为多个时
func swap(x, y string) (string, string) {
   return y, x
}
//大多数工程的api函数都返回两个值,一个是实际值,一个是错误信息
bodyText, err := ioutil.ReadAll(resp.Body)

2.11 指针

与C类似,不多说了。

但是注意,go中的空指针为:nil

var ip *int //声明指针变量
//使用案例
c := add2ptr(&n)
//执行完n就直接加2了
func add2ptr(n *int) {
    *n += 2
    return *n
}

2.12 结构体

与C++就是一样的。

//结构体的声明
type user struct {
    name     string
    password string
}
//结构体的初始化
a := user{name: "wang", password: "1024"}
b := user{"wang", "1024"}
c := user{name: "wang"}
//结构体对象的成员进行修改
c.password = "1024"
var d user
d.name = "wang"
d.password = "1024"

PS:结构体也能支持指针,这样能够实现对于结构体的修改,也可以在某些情况下避免一些大结构体的拷贝开销。

【结构体方法】

func紧跟结构体对象的,就是结构体方法。

type user struct {
    name     string
    password string
}
//不带指针的方法实际是一种拷贝
func (u user) checkPassword(password string) bool {
    return u.password == password
}
//带指针的方法回对成员造成实际的修改
func (u *user) resetPassword(password string) {
    u.password = password
}

2.13 错误处理

函数那一小节的时候提到过一嘴。我们可以在编码中通过实现 error 接口类型来生成错误信息。

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")
}

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

2.14 字符串操作

go提供了字符串操作的丰富方法

a := "hello"
//查询字符串是否包含ll
fmt.Println(strings.Contains(a, "ll"))                // true
//查询字符串中有几个l
fmt.Println(strings.Count(a, "l"))                    // 2
//查询字符串是否有前缀he
fmt.Println(strings.HasPrefix(a, "he"))               // true
//查询字符串是否有后缀llo
fmt.Println(strings.HasSuffix(a, "llo"))              // true
//查询ll所在字符串的位置
fmt.Println(strings.Index(a, "ll"))                   // 2
//Trim用于删除字符串str头或尾的指定字符
input = strings.Trim(input, "\r\n")//去掉换行符(若linux下是\n)
//用 - 来拼接字符串
fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
//重复
fmt.Println(strings.Repeat(a, 2))                     // hellohello
//替换
fmt.Println(strings.Replace(a, "e", "E", -1))         // hEllo
//分割
fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]
//转换大小写
fmt.Println(strings.ToLower(a))                       // hello
fmt.Println(strings.ToUpper(a))                       // HELLO
fmt.Println(len(a))                                   // 5--字符串长度

2.15 字符串格式化&字符串转换

不同与c中printf中需要区分数据类型加%,这里一个%v就行,并且,如果是+则表示较详细的信息,如果是#则表示更详细的信息。如下面所示:

fmt.Printf("s=%v\n", s)  // s=hello
fmt.Printf("n=%v\n", n)  // n=123
fmt.Printf("p=%v\n", p)  // p={1 2}
fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}

【转换】标准库中的"strconv"专用用于字符串的数字转换

可以详细解析到指定类型或者利用Atoi快速转换

//para: 字符串,返回什么精度的数字
f, _ := strconv.ParseFloat("1.234", 64)
//para: 字符串,进制(0,则会自动推测), 返回什么精度的数字
n, _ := strconv.ParseInt("111", 10, 64)
​
//快速转成数字
n2, _ := strconv.Atoi("123")
fmt.Println(n2) // 123
//此处也可以用Itoa快速转成字符串

2.16 json处理

go语言里面的JSON操作非常简单,主要是结构体与字符串的相互转换(类似一个编解码的过程)!

对于一个已有的结构体,我们可以什么都不做,只要保证每个字段的第一个字母是大写,也就是是公开字段。那么这个结构体就能用JSON.marshaler 在序例化,变成一个JSON的字符串。

例如结构体:

type userInfo struct {
    Name  string
    Age   int `json:"age"`//这一段是可以替换的,如下面json就输出小写的key了
    Hobby []string
}
a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}

结构体转化成字符串

buf, err := json.Marshal(a)//这就完成转化了
//buf, err = json.MarshalIndent(a, "", "\t")这个是成结构的转化成字符串
fmt.Println(buf)         // [123 34 78 97...]
//注意需要强转string才会输出正确
fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}

字符串转结构体

var b userInfo
err = json.Unmarshal(buf, &b)

2.17 对时间的操作

用的时候现查表即可

now := time.Now()//获得当前时间
//构造时间(带时区)
t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) 
// 2022 March 27 1 25
fmt.Println(t.Format("2006-01-02 15:04:05"))//以...格式打印                   
// 2022-03-27 01:25:36
diff := t2.Sub(t)//时间减法
fmt.Println(diff)                           // 1h5m0s
fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
//Go语言中的Parse()函数用于解析格式化的字符串,然后查找它形成的时间值。此外,此函数在时间包下定义。
//在这里,layout通过以哪种方式显示参考时间(属于一个模板的意思)来指定格式
//这个可以有第三个变量,用以转换时区(time.Local)
t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
now.Unix()//获得时间戳

2.18 读取系统的环境变量

//获得进程在执行时的命令行参数
os.Args
//下面分别为获取环境变量&写入环境变量
os.Getenv("PATH")
os.Setenv("AA", "BB")
//可以通过exec.Command快速启动子进程,并获得其输入输出
//Go标准库提供了一个可以很容易地运行外部命令的方法
buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()

三、实战项目总结

3.1 猜数字

学习到的知识点:

  1. 首先对于随机数,要记得设置随机种子(常用时间戳代替)
  2. 如何读取bash中的输入(字符流转整型)
reader := bufio.NewReader(os.Stdin)// 将bash的输入转换为一个只读的流
input, err := reader.ReadString('\n')//这个就从这个流中读取一行(即读到换行符)
input = strings.Trim(input, "\r\n")//去掉刚刚也读入的换行符(若linux下是\n)
guess, err := strconv.Atoi(input)//最后转整型

3.2 在线词典

通过在终端执行文件时输入的参数不同,进行模拟的POST请求来模拟浏览器来接收翻译的api结果。

随后,将返回来的请求结构,先通过io.ReadAll读成string字符串,随后将该字符串化成结构体展示出来

【易错点】复制post请求时,要选择以bash格式复制(而不是以bash格式复制全部内容)

3.3 Socks5 代理

这个好难,先基本了解。

四、作业

4.1 猜数字

直接一个整形读取就可以了。用下面这个代替上面四行代码

var guess int;
_, err := fmt.Scanf("%d", &guess)

4.2 网上字典

  1. 对于不同的引擎,其实要处理的只有变量req,因此我们提前声明,随后加一个switch结构,根据命令行的第三个参数区别初始化即可
  2. 就是同时开两个 goroutine 即可。