这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。
从Go语言特性入手,讲述了Golang区别于其他语言的优点,通过Go by Example对基础语法进行了巩固学习。
然后对Golang常用标准库的使用和注意事项通过代码示例,最后是命令行词典和socks5代理两个小项目的实践,进一步了解Golang。
Go语言特性
- 高并发,高性能
Golang是基于其他语言的痛点,开发的新时代语言。
天生支持高并发,能够充分利用多核CPU,高性能开发简单。 - 简单易学
- 标准库,工具链丰富
- 静态链接,快速编译
- 跨平台
多平台,多系统,可交叉编译,无需配置编译环境。 - 垃圾回收
自动管理内存释放,专注业务逻辑编程。
应用场景
- 公司
使用Golang开发的公司越来越多,比如国内的ByteDance,Tencent,美团,七牛云等,国外的Google,Facebook等。 - 领域
Golang在云计算,微服务,大数据,区块链,物联网等领域都有广泛的使用,其中在云计算,微服务领域有非常高的市场占有率。 - 云原生组件
Docker,Kubernetes,etcd,Prometheus等全是通过Golang实现。
重点语法
通过Go by Example进行入门上手,对重点语法进行总结归纳。下面展示核心代码部分,完整源码,见Github仓库
数据类型
- string支持
+拼接,也可以通过fmt.Sprintf(),strings.Join()和buffer.WriteString(),build.WriteString()进行拼接。
str1, str2 := "go", "lang"
fmt.Println(str1 + str2)
fmt.Println(fmt.Sprintf("%s%s", str1, str2))
fmt.Println(strings.Join([]string{str1, str2}, ""))
var bt bytes.Buffer
bt.WriteString(str1)
bt.WriteString(str2)
fmt.Println(bt.String())
fmt.Println("go" + "lang")
var build strings.Builder
build.WriteString(str1)
build.WriteString(str2)
fmt.Println(build.String())
- slice比array更常用,slice可以组成多维数据结构,内部长度可以不一致。slice可以模拟
stack,queue。 - map的键值对是无序的,可以自行按照
key或value进行排序。检测key是否存在:
m := make(map[string]int)
if v, ok := m["zhangsan"]; ok {
fmt.Println("key exists, value:", v)
}
- range可对slice,map,string进行遍历,第一个参数是index(key),第二个参数是value。
slice := []int{1, 3, 4, 6}
for i, v := range slice {
fmt.Println(i, v)
}
- Go支持指针,允许通过引用传递来传递值和修改值。
值传递是数据的拷贝,会分配新的内存存储;
引用传递是指针的拷贝,共享数据的内存地址,通常使用指针是为了修改值或节约内存空间。 - string等价于
[]byte,Golang中字符是rune类型(uint8), 用单引号括起来的值。 - struct是带类型的字段集合,是可变的,通过构造函数封装创建结构体实例是习惯用法。
type User struct {
Name string
Age int
}
func Constructor() User {
return User{Name: "zhangsan", Age: "22"}
}
函数
- 对于多返回值函数,可用空白标识符
_仅返回需要的值:
func calc() (sum, sub int) {
return 1+2, 1-2
}
sum, _ := calc()
- 变参函数的使用:
func sum(nums ...int) int {
ans := 0
for _, num := range nums {
ans += num
}
return ans
}
ans := sum(1, 2)
ans = sum(1, 2, 3)
// 也可以传slice
nums := []int{1, 2, 3, 4}
ans = sum(nums...)
- interface是方法签名的集合,在Go中实现一个interface就是实现其中所有方法。
如果一个变量实现了接口,就可以调用接口的方法。 - 可以为值类型或指针类型的接收者定义方法,想避免调用方法时产生数据拷贝或想修改接收者的值,可以选用指针来调用方法。
type User struct {
Name string
Age int
}
func (u User) ModifyInfo1() User {
u.Name = "lisi"
u.Age = 15
// u修改不成功
return u
}
func (u *User) ModifyInfo2() *User {
u.Name = "lisi"
u.Age = 15
// u修改成功
return u
}
- Go习惯用独立的,明确的返回值来传递错误信息。函数的最后一个返回值常常是error类型的错误,
errors.New使用给定的错误信息构造一个基本的error值。也可以使用Error()方法,自定义error类型。
在if的同一行进行错误检测是Go常用的方式。如果想在程序中使用自定义错误类型的数据,需要通过类型断言得到这个自定义错误类型的实例。
常用标准库
fmt
输出:PrintXXX, FprintXXX, SprintXXX, ErrorfXXX
fmt.Print("不换行")
str := "golang"
fmt.Printf("%s\n", str)
fmt.Println("有换行")
// Fprint将内容输出到io.Writer类型的变量中,常用在写入内容到文件
fileObj, err := os.OpenFile("./xx.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println("打开文件错误,err:", err)
}
fmt.Fprintf(fielObj, "向文件中写入:%s", str)
fmt.Fprintln(os.Stdout, "向标准输出写入内容")
// Sprint把传入的数据生成并返回一个字符串
s1 := fmt.Sprint("golang")
s2 := fmt.Sprintf("s1:%s", s1)
s3 := fmt.Sprtinln("golang有换行")
// Errof根据format参数生成格式化字符串并返回一个包含该字符串的错误, 通常用来自定义错误类型
err := fmt.Errof("这是一个自定义错误类型")
输入:ScanXXX
var name string
var age int
fmt.Scan(&name, &age)
fmt.Scanf("%s, %d\n", &name, &age)
fmt.Scanln(&name, &age) // 遇到换行才停止
// 如果想要获取包含空格等完整输入内容,可以用bufio.NewReader
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString("\n")
text = strings.TrimSpace(text)
fmt.Printf("%#v\n", text)
输入还有FscanXXX从io.Reader读取和ScanXXX从指定字符串读取。
time
时间戳:
now := time.Now()
timestamp1 := now.Unix() // 时间戳
timestamp2 := now.UnixNano() // 纳秒级时间戳
时间间隔:time.Duration表示1ns,time.Second表示1s
时间格式化,以Go的诞生时间固定格式模板:
now := time.Now()
fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
strconv
string和int的转换:
num := strconv.Atoi("123")
str := strconv.Itoa(num)
ParseXXX将字符串转换为给定类型:
boolType, err := strconv.ParseBool("true")
intType, err := strconv.ParseInt("23", 10, 64) // 10进制,int64
floatType, err := strconv.ParseFloat("3.14", 64) // float64
uintType, err := strconv.ParseUint("23", 64) // uint64
FormatXX将给定类型转换为string。
strings
包括了操作字符的常用函数,比如大小写转换,是否有指定前缀或后缀,字符串拼接或分离等。
str := "golang"
strings.Contains(str, "lang")
strings.HasPrefix(str, "go")
strings.ToUpper(str)
strings.Replace(str, "g", "G", -1)
encoding/json
这个包实现了对json的编码和解码。
// 序列化,其他数据类型转json
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
u := User{Name:"zhangsan", Age:"18"}
buf, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(buf))
// 反序列化,json转其他数据类型
var u2 User
err = json.Unmarshal(buf, &u2)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", u2)
实战练习
命令行词典
需求:调用第三方翻译API实现一个命令行词典,使用格式:simpleDict word
分析:整个过程主要是三个步骤:发送HTTP请求,解析响应json,打印出结果。
- 抓包,打开彩云小译,按F12,输入你要翻译的单词,比如
translate, 可在DevTools的Network查看HTTP的post请求信息:
- 生成代码,右键
copy as curl(bash),复制curl命令。在终端可以返回这个请求的json代码,我们直接使用在线curl2go生成该请求的Golang代码, 运行该代码同样能得到请求的json代码,也就是服务器返回给我们的响应体信息:
- 从代码可以知道我们需要构造相应的request和response结构体,response结构体可以通过oktools在线生成,只需将DevTools中response的
json复制粘贴,转换嵌套即可:
- 从结构体当中找到我们需要返回打印的指定字段值,完善优化修改代码即可。
- 同理,对火山翻译和有道智云两个API进行Golang代码生成,其他第三方API也是同样的道理,只不过可能request和response的相关信息不好找或者经过Hash等处理。
- 最后实现了三方引擎并行的一个命令行翻译词典,运行结果如下图,完整源码:
proxy
socks5代理协议,是明文传输,它的用途主要是作为一道屏障隔绝内部网络与互联网,并且使两者联系起来。比如某些企业的内网为确保安全性,有严格的防火墙策略,带来的副作用就是访问某些资源很麻烦。socks5相当于在防火墙开了一个口子,让授权的用户,可以通过单个端口去访问内部的所有资源。
同样地,网络爬虫在爬取过程中,很容易遇到IP访问频率超过限制,为了突破限制,人们就会去网上找代理IP池,这些代理IP池里面的很多协议就是socks5协议。
socks5代理过程:
主要分为4个阶段:
- 握手阶段:
浏览器向socks5代理发送请求,包的内容包括了一个协议的版本号,支持的认证种类,socks5服务器会选中一个认证方式,返回给浏览器,如果返回的是00就代表不需要认证,返回其他类型会开始认证流程,对认证流程不再详述; - 认证阶段:
认证阶段作为协商的一个子流程,它不是必须的。socks5服务器可以决定是否需要认证,如果不需要认证,那么认证阶段会被直接略过。
如果需要认证,客户端向socks5服务器发起一个认证请求,包括:版本, 用户名长度,对应用户名的字节数据,密码长度,密码对应的数据。
socks5服务器收到客户端的认证请求后,解析内容,验证信息是否合法,然后给客户端响应结果,包括:版本,状态,如果STATUS字段为0x00表示认证成功,其他的值为认证失败。当客户端收到认证失败的响应后,它将会断开连接。 - 请求阶段:
认证通过以后,浏览器会向socks5服务器发起请求,主要信息包括版本号,请求的类型,主要是connection请求,代表代理服务器要和某个域名或某个IP地址某个端口建立TCP连接,代理服务器收到响应后,会真正和后端服务器建立连接,返回一个响应; - relay阶段:
浏览器正常发送请求,代理服务器收到请求,将请求转换到真正的服务器,真的服务器返回响应,也会给代理服务器转发给浏览器,代理服务器不关心流量的细节,可以是HTTP流量,也可以是TCP流量,这就socks5的工作原理。
需求:启动程序后,通过浏览器配置代理,代理服务器的日志会打印出你访问的网站的域名或者IP,也就是你的网络流量都会经过这个代理服务器。
分析:
- tcp echo server
服务端监听你的信息并将其打印。 - auth
浏览器会给代理服务器发送一个包,包括三个字段version,methods,methods的编码,代理服务器需要返回一个response,包括两个字段version,method。 - request
读取携带的URL或IP+端口的包,并把它打印。
请求包,包括6个字段,version,command,RSV,atype,addr,port。 - relay
用net.dial建立一个TCP连接,并启动两个goroutine,通过io.copy建立浏览器和下游服务器的双向数据转发。
需要使用context机制,等待ctx.Done()避免connect函数立刻返回,连接关闭。 - 通过
switchOmega插件,实现网络流量代理,完整源码:
总结
本篇主要对Golang基础的重点易错点,几个常用标准库的使用方法进行了总结记录,并且通过两个小项目对Go语言上手实践,新手丝滑入门Golang。
第一篇blog,大纲目录比较乱,总是修修改改,后边找个时间整理一下写blog的过程(挖坑)。欢迎大家评论转载,多多发表自己的意见或建议。
(ps:blog写得真慢,要搞快点!!!)