Go语言简单上手 | 青训营笔记

111 阅读10分钟

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

课程重点

  1. 安装Golang及其开发环境(IDE)
  2. 什么是Go?& Go语言基础语法
  3. 通过三个小项目深入了解Go语言

1. 安装Golang及其开发环境

Go是一门需要编译的编程语言,所以编译器和开发环境(如果你不想用记事本写代码)是必须的

  • 1.1 安装编译器

用浏览器打开 studygolang.com/dl ( 会魔法的同学也可以打开go.dev )

选择自己电脑的系统下载对应的安装包安装即可

这里以Windows为例,下载go1.19.5.windows-amd64.msi安装即可

屏幕截图 2023-01-15 162824.png

  • 1.2 安装集成开发环境

Go语言可用的集成开发环境有很多(例如VSCode或者Goland)

可以选择自己喜欢的IDE进行下载,这里为下载VSCode的同学提醒一下:记得安装Go拓展

到这里, 我们就可以打开编辑器愉快的开始进行Go语言的开发啦!

注意:

当我们在下载Go语言的一些包的时候,有些同学可能会报错安装失败

我们只需要打开终端输入以下两行命令即可

go env -w GO111MODULE=on 
go env -w GOPROXY=https://goproxy.cn,direct

2.什么是Go?& Go语言基础语法

Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易

(优点就不列啦,太多了)

  • 2.1 Go语言基础语法

在了解基础语法之前,首先我们要了解Go语言的语言结构是什么样的

在Go的代码中通常包含了以下部分:

  • 包声明
  • 引入包
  • 函数

这里以一个简单的HelloWorld为例

package main      <-包声明(main包是程序的入口包,程序代码执行会从这里开始)
  
import (
    "fmt"         <-引入包(将需要用到的包丢到括号里就可以啦,一行一个哦!)
) 
  
func main() {     <-函数( 函数通常以func定义,main函数是程序运行时执行的第一个函数)

   fmt.Println("Hello, World!")  
}

于是,当我们在终端中执行 go run (你的代码路径)

Hello,World!就成功的显示在我们的屏幕上了

  • 变量

在Go语言中声明一个变量可以有很多种方法:

var a = "init"     //哇!有个a变量(雾)
var b, c int = 1, 2  //b = 1 , c = 2
d := true

Go是一门强类型语言,变量是有数据类型的(虽然也可以自动识别)

一般情况下,Go语言的数据类型声明在变量名之后

  • 常量

常量跟变量很像,只是多了个const前缀

const a string = "Hello"
  • 运算

字符串是Go的内置变量类型,可以直接进行运算哦!

a = "Hello "
b = "World!"
c := a + b   //c = "Hello World!"
  • 循环

在Go中,循环只有一种形式 —— for循环

可以直接写条件,也可以像C++的for循环那样,但是Go语言中for后是没有括号的

for {   //无限循环(死循环)
    fmt.Println("Hello Go!")
}

for i < n {   //带条件的循环
    fmt.Println("Hello Go!")
}

for i = 0; i < n; i++ { //C++式循环
    fmt.Println("Hello Go!")
}

不知道有没有人注意到,Go语言的每一条语句后都是没有分号(;)的

因为编译器会自动的识别换行符作为语句结束的标志,因此我们在写循环时

不可以将for后的大括号写在下一行(if也是同理)

  • 判断

判断语句同Go的循环语句一样,条件是不需要用括号括起来的

if i > n {
    fmt.Println("Hello!")
} else {
    fmt.Println("Go!")
}
  • switch语句

Go的switch语句与C语言的写法基本一致,但是不同点是,每一个case内的语句结束后不需要break语句,Go语言会自动终止,而不是像C语言那样将之后所有的语句执行完

switch number {
case 1:  //number为1时执行
    fmt.Println("case 1")
case 2:  //number为2时执行
    fmt.Println("case 2")
default:  //number非以上情况时执行
    fmt.Println("Hello Go!")
}
  • 数组

Go的数组一般不常用,因为长度不可变,下一个介绍的切片才是最常用的功能

var a [5]int   //声明长度为5的整数型数组a
a[2] = 100   //将数组a的第三位树赋值为100
b := [5]int {1,2,3,4,5}
var c [5][5]int  //二维数组
  • 切片

本质上也可以说是数组。但是长度可变,非常好用!!

a := make([]string, 3)  //创建长度为3的字符串型切片
a[0] = "aaa"            //将切片a的第一位赋值为aaa
a = append(a,"bbb","ccc")  //在切片a后增加两位,分别为bbb和ccc

注意:

append会返回一个新的切片,所以应当再次把他赋值给a才能完成对a的拓展

  • Map

Map类似于词典或者哈希表,存储key - value的对应关系

a := make(map[string]int)  //创建Map存储字符串(key)和整数(value)的对应关系
a["one"] = 1  //定义对应关系

fmt.Println(a[1])  //输出one
fmt.Println(a["one"])  //输出1
  • range

range用于循环中对数组,切片或者Map进行遍历,用法如下

nums := {1,2,3,4}
for index, num := range nums {   //index为索引,num为值
    fmt.Println(num)   //输出 1234
}

range函数在运行中会返回两个值,第一个是当前位置的索引(在数组的哪个位置)

第二个是当前位置的值

而在遍历Map时,第一个则是Key 第二个是Value

不需要的值可以填入下划线来忽略

  • 指针

听到指针,部分学C/C++的同学就要站起来了,但是此指针非彼指针

Go语言的指针能执行的操作非常有限,举两个例子来说明一下吧

func add2(a int) {
    n += 2
}

在这段代码中,我们想要实现一个给传入的数+2的操作,但是由于这是以形参的形式传入的,所以上面的这段代码可以说加了个寂寞

func add2(a *int) {
    n += 2
}

add2(&n)

但是加了*就大不一样了,这是函数可以根据内存地址直接对n进行操作

同样的,传入参数也要加&表示传入指针(内存地址)

  • 结构体

可以格式化的储存数据

type user struct {  //创建User结构体
    UserName string  //格式:数据名 数据类型
    PassWord string
}

var user1 user   //用user结构体创建uesr1
user2 := user{"李四","pwd"}   //带参构造方法
user1.UserName = "张三"   //结构体数据操作
  • 函数 & 错误处理

说了这么多,来说说Go语言程序最基础的单位——函数

Go的函数以func声明,返回值在函数名之后声明,传入值也是一样先名称后类型

不声明返回值则默认为无返回值类型

func add(a int, b int) int {
    return a + b
}

Go语言有一个很有趣的特性——多返回值

这一特性在错误处理中有很多应用,例如:

func findUser(users []user, name string) (target *user, err error) {
    for _,u := range users {  //遍历数组寻找用户
        if u.name == name {
            return &u, nil    //找到了返回用户和空的错误(nil在Go中是空的意思)
        }
    }
    return nil, errors.New("Not found")  //没有找到返回空用户和错误
}

我们只需要后续加以判断即可在出错时终止程序,防止更大的错误

  • 字符串操作

这里倒是没啥好说的,看代码

a := "hello"
fmt.Println(strings.Contains(a,"l1"))                // true
fmt.Println(strings.Count(a,"1"))                    // 2
fmt.Println(strings.HasPrefix(a, "he"))              // true
fmt.Println(strings.HasSuffix(a,"1lo"))              // true
fmt.Println(strings.Index(a,"11"))                   // 2 
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
  • 字符串格式化
p := point{1,2}
fmt.Printf("p=%v\n",p)          // p={1 2}
fmt.Printf("p=%+v\n",p)         // p={y:1 y:2}
fmt.Printf("p=%#v\n",p)         // p=main.point{x:1, y:2}
  • JSON处理

对于名称首字母都大写的结构体可以执行JSON处理

type user struct {  
    UserName string `json:"name"` //使用这样的写法可以将json中的UserName改为name
    PassWord string
}
js, err := json.Marshal(user)    //JSON格式化
umjs, err := json.Unmarshal(js)  //去格式化
  • 时间

引入time包可以进行时间操作

now := time.Now()  //获取现在时间
fmt.Println(now)  // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
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)     // 2022-03-27 01:25:36 +0000 UTC
fmt.Println(t.Year(),t.Month(),t.Day(),t.Hour(),t.Minute()) //获取指定的数字
fmt.Println(t.Format("2006-01-02 15:04:05"))  //格式化为:2022-03-27 01:25:36 字符串
diff :=t2.Sub(t)    //两个时间点相差的时间段
fmt.Println(diff)   //1h50m0s
fmt.Println(diff.Minutes(),diff.Seconds())   // 65 3900
t3, err := time.Parse("2006-01-02 15:04:05""2022-03-27 01:25:36") //把字符串解析回时间
fmt.Println(t3==t)   //true
fmt.Println(now.Unix())  //获取时间戳
  • 数字处理

strconv包可以帮助我们对数字进行一些处理

strconv.ParseFloat("1.234",64)  //1.234
strconv.ParseInt("123",0,64)  //123

这两个函数可以用于将字符串转换为数字类型,传入值第一个为字符串,第二个是进制数(如10进制,填0表示自动推测)第三个是转换的精度(64位)

或者我们也可以快速转换

strconv.Atoi("123") //字符串转数字
strconv.Itoa(123)   //数字转字符串

注意:

以上关于数字处理的所有函数均有两个返回值,第二个为错误类型!

  • 进程信息
os.Args  //数组类型,第一位为运行路径 往后为启动时附带的参数
os.Setenv("AA","BB")  //设置环境变量
os.Getenv("AA")       //获取环境变量

3.通过三个小项目深入了解Go语言

  1. 猜数游戏
package main

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

func main() {
	rand.Seed(time.Now().Unix())  //设定随机数种子
	target := rand.Intn(100)      //获取随机数(最大为100)
	reader := bufio.NewReader(os.Stdin)  //创建Reader将输入转换为流
	for {
		fmt.Println("Please enter your answer: ")
		input, err := reader.ReadString('\n')   //读取一行输入
		if err != nil {
			fmt.Println("An error occrued while reading", err)
			continue
		}
		input = strings.Trim(input, "\r\n")   //去除换行符
		ans, err := strconv.Atoi(input)
		if err != nil {
			fmt.Println("Invalid input, Please enter an integer")
			continue
		}
		if ans > target {   //判断大小
			fmt.Println("It's too big, try again")
		}else
		if ans < target{
			fmt.Println("It's too small, try again")
		}else
		if ans == target{
			fmt.Println("Correct, congratulations!! ")
			break
		}
	}
}

  1. 网络词典

解决思路:

  • 通过浏览器调试观察正常翻译请求
  • 通过网站Convert curl commands to Go将请求转化为代码
  • 通过网站JSON转Golang Struct生成返回值结构体
  • 编写请求内容结构体将自定义的请求内容发送至翻译网站
  • 将返回值去格式化写入结构体中
  • 获取结构体中的翻译和音标部分
  • 输出到屏幕,完成!

代码太长就不放了,可以自行去Github克隆(紫色的是链接,可以点的!)

  1. Socket5代理服务端

在开始之前,我们需要了解Socket5代理是怎么工作的:

屏幕截图 2023-01-15 195116.png

这里的每一步都需要我们使用代码来实现

server, err := net.Listen("tcp", "127.0.0.1:1080")  //监听端口
if err != nil {
	panic(err)
}
for {
	client, err := server.Accept()    //接受连接
	if err != nil {
		log.Printf("Accept failed %v", err)
		continue
	}
	go process(client)    //启动子线程进行后续操作
}
func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	err := auth(reader, conn)   //调用认证函数
	if err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
	err = connect(reader, conn)  //调用连接函数
	if err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
}

首先是认证协商阶段

Socket5在连接时客户端会发送数据到服务端,包括:

  • Socket5版本
  • 支持的认证方法数
  • 认证方法

我们使用的是无需认证,选择 00 方法即可

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
	ver, err := reader.ReadByte()  //因为内容是有序的,前两部分各占用1字节,所以按字节读取
	if err != nil {
		return fmt.Errorf("read ver failed:%w", err)
	}
	if ver != socks5Ver {  //验证版本
		return fmt.Errorf("not supported ver:%v", ver)
	}
	methodSize, err := reader.ReadByte()  //读取方法数
	if err != nil {
		return fmt.Errorf("read methodSize failed:%w", err)
	}
	method := make([]byte, methodSize)  //读取方法
	_, err = io.ReadFull(reader, method)
	if err != nil {
		return fmt.Errorf("read method failed:%w", err)
	}
	_, err = conn.Write([]byte{socks5Ver, 0x00}) //返回认证成功数据
	if err != nil {
		return fmt.Errorf("write failed:%w", err)
	}
	return nil
}

接下来需要读取客户端的连接请求,并向目标服务器发送请求

连接请求包含:

  • Socket5版本
  • 请求类型 (通常是CONNECT请求)
  • 保留字段
  • 目标地址类型 (域名,iPv4,iPv6)
  • 目标地址
  • 目标端口

连接完成后向客户端发送回执并转发来自目标服务器的数据

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
	buf := make([]byte, 4)  //读取前四个请求数据
	_, err = io.ReadFull(reader, buf)
	if err != nil {
		return fmt.Errorf("read header failed:%w", err)
	}
	ver, cmd, atyp := buf[0], buf[1], buf[3]  //划分数据
	if ver != socks5Ver {  //验证版本信息
		return fmt.Errorf("not supported ver:%v", ver)
	}
	if cmd != cmdBind {
		return fmt.Errorf("not supported cmd:%v", ver)
	}
	addr := ""
	switch atyp {  //根据请求类型读取目标地址
	case atypIPV4:
		_, err = io.ReadFull(reader, buf)  //ipv4
		if err != nil {
			return fmt.Errorf("read atyp failed:%w", err)
		}
		addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
	case atypeHOST:
		hostSize, err := reader.ReadByte()  //域名
		if err != nil {
			return fmt.Errorf("read hostSize failed:%w", err)
		}
		host := make([]byte, hostSize)
		_, err = io.ReadFull(reader, host)
		if err != nil {
			return fmt.Errorf("read host failed:%w", err)
		}
		addr = string(host)
	case atypeIPV6:
		return errors.New("IPv6: no supported yet")  //ipv6(暂不支持)
	default:
		return errors.New("invalid atyp")
	}
	_, err = io.ReadFull(reader, buf[:2])  //读取端口信息
	if err != nil {
		return fmt.Errorf("read port failed:%w", err)
	}
	port := binary.BigEndian.Uint16(buf[:2])

	dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
	if err != nil {
		return fmt.Errorf("dial dst failed:%w", err)
	}
	defer dest.Close()
	log.Println("dial", addr, port)   //向终端打印输出信息
	_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})  //向客户端发送连接成功回执
	if err != nil {
		return fmt.Errorf("write failed: %w", err)
	}
	ctx, cancel := context.WithCancel(context.Background())   //利用context在连接中断前能保持转发
	defer cancel()

	go func() {
		_, _ = io.Copy(dest, reader)  //向目标服务器转发内容
		cancel()
	}()
	go func() {
		_, _ = io.Copy(conn, dest)   //向客户端转发内容
		cancel()
	}()

	<-ctx.Done()
	return nil
}

今日的学习内容到这里就结束了,我们明天再见!