之前在 Cisco 的时候,我是一名披着网络工程师头衔的后端程序员,一直使用的主力语言是 Python。然后在春节前后开始学习 Go,? 了一年多的想法终于付出了行动,也终于努力取得了一些小成果——和 Nova 大佬?♂️一起创造了 WebP Server Go
下面的内容就以一个 Python 程序员的观点与视角去介绍、了解 Golang,某些地方可能描述不准确或有错误,还望纠正。内容可能并无法方方面面的覆盖 Golang 的各种细节,只是方便从 Python 过来的程序员迅速上手 Go。
为啥要学习 Golang,Golang 与 Python 有何不同
每个人学习某种语言都有自己的一些原因,对于我本人而言,一个非常重要的原因是,如果只会一门语言,那是不是说出去有点太寒碜了?JavaScript 的话,其实也算会,但是毕竟是做后端的,NodeJS 还真就不太行,各种前端框架也不太熟悉。
那既然想多学一门语言,为什么又偏偏选择 Go,而不是 Java,C/C++ 等久经沙场的语言呢?
我的一个考量因素是,Golang 拥有比较强的交叉编译能力,性能要好一些。尽管它还很年轻,2009 年才正式发行,但是看看出身吧。出身豪门,还努力,这真是励志呢。
好吧,废话少说,先说 Python 吧。Python 是一门动态的、强类型的脚本语言;Golang 是一门静态的、强类型的编译语言,Golang 天生支持并发,没有 GIL 锁
编程语言的基础都是数据类型,就像人类语言的基础是词汇一样。下面就从数据类型开始说起吧。
基本数据类型
数值类型
Python 的数值类型就太简单了,直接一个 int 和 float,长度什么的完全看内存。
Gol 就有些不同,数字、浮点,包含有符号和无符号两种类型,同时可选不同大小,如 int8,int16 等,是不是有点回想起了上学时试卷上 C 语言溢出的问题?
Int32 就是 rune,rune 就是 utf-8 的 code point
字符串类型
Python 的字符串用单引号、双引号、三单、三双都可以表示,从含义上来说是没有区别的。
Go 的字符串要复杂一些,有以下情况:
使用双引号表示,\n
等字符会被解释为换行符;
原生字符串,使用反引号 `` 表示,类似 Python 中的r
前缀,其中的\n
等字符就是表示该字符,而不是换行符,并且反引号表示的字符串可以任意换行;
单引号表示 rune literal,和 C 语言中的 char 基本差不多(比如 A 就是 65,还记得吧)
观察以下代码
- var eng = "abc"
- var chs = "你好吗"
- fmt.Println(len(eng))
- fmt.Println(len(chs))
输出分别为 3 和 9。
不同于 Python,python 中这两者返回值都会是 3,因为一共就 3 个字符嘛,管你中文英文的;golang 中len
表示字节数,由于在 UTF-8 中文使用三子节表示,所以三个汉字对应为 9。
如要实现类似 Python 中的 len,那么需要使用utf8.RuneCountInString(eng)
,计算实际字符数量。不过遇到了 👨👩👧👦 这种四个 emoji+3 个零宽字符时结果是 7
由于这一点的不同,在遍历时也会有类似的问题。一个需要注意的事情就是,遍历的核心是要准确的 “切割” 每个字,所以在chs
这个字符串中,要遍历三次。
遍历字符串,用 range 循环的话
- for k, v := range chs {
- fmt.Println( k, v)
- }
循环会进行 3 次,因为一共时三个字;k 表示索引,v 的值实际上是 “一串数字 “,而不是我们预期的单个字符 “你”。可以用 string 转换,或者%c
量词
用下标的话,按照常规的想法,我们会这样做
- for i := 0; i < len(chs); i++ {
- fmt.Println( chs[i])
- }
❌❌❌实际上循环会进行 9 次,这就错了
正确的姿势应该是转换为 rune 数组
- var c = []rune(chs)
- for i := 0; i < len(c); i++ {
- fmt.Println(c[i])
- }
这样就是三次循环了。或者这样做切片
- for i := 0; i < len(chs); {
- r, size := utf8.DecodeRuneInString(chs[i:])
- i += size
- fmt.Println(string(r))
- }
DecodeRuneInString
会把字符串解码成 “数字”。
是不是觉得很懵圈,这是什么垃圾玩意!哎,反正用range
就没毛病了。
如果要取第一个字 “你”,用chs[0]
是错误的,用chs[0]
实际上访问的是前 8 个字节,对于 ASCII 字符来说自然没问题,但是其他字符就有问题了。
所以要取第一个字符咋办啊?转换为 rune 数组啊
- var c = []rune(chs)
- fmt.Println(c[0])
如果要取最后一个字符,不可以像 Python 一样用 - 1,要用c[len(c)-1]
参考阅读 《从 golang 字符串 string 遍历说起:聊聊 go 语言的 Strings、bytes、runes 和字符》
字符串常见操作,如搜索、替换、比较基本上都包含在了 strings 包中;
类型转换,包含在strconv
中,如:
Atoi
字符串转数字,Itoa
数字转字符串
或者更通用,用 Format 数字转字符串、Parse 字符串转数字类型,举例如下,注意返回两个制,第二个是 err
- fmt.Println(strconv.ParseInt("4567",10,0))
- fmt.Println(strconv.ParseInt("10",2,0))
- fmt.Println(strconv.FormatInt(1234,10))
第二个参数base
为进制,
一般的,base
的取值为 2~36,如果 base
的值为 0,则会根据字符串的前缀来确定 base
的值:"0x" 表示 16 进制; "0" 表示 8 进制;否则就是 10 进制。
第三个参数bitSize
表示结果的位宽(包括符号位),0 表示最大位宽。8 就是 int8
转个类型都要这么多参数,记不住咋办!,知道自己在干嘛的话,无脑 00 吧(( ̄◇ ̄;) 既然都无脑了,可能也不知道自己在干嘛……
同理,也有ParseFloat
,用法与这个基本一致。
常量
Python 中没有常量的概念,我们一般用大写表示 “常量”。
Golang 中使用const
定义常量,如 const pi = 3.14
,定义多个常量可以这样
- const (
- a = 1
- b
- c = 3
- d
- )
b 和 d 留空,意味着会取它上面的那个常量的值,也就是 1 和 3
使用常量生成器 iota 可以生成相关值的常量,如下代码
- const (
- a = iota
- b
- c
- d
- )
值分别为 0123
甚至还可以这么玩,这就是有点神仙操作了
- const (
- _ = 1 << (iota * 10)
- KiB
- MiB
- GiB
- )
顺便说一句,定义变量的时候,a:=3
, var a=3
, var a int
的区别咱就不说了。
复合数据类型
数组
Python 中,其实也是有 array 这种类型的,类型都是固定的,而且还可变
- from array import array
- a = array('l', [1, 2, 3, 4, 5])
- a.append(5)
- a[3] = 999
在上述代码中,每一个元素都要是整型。一般来说,我们会用 list 来表示数组,只不过 list 的元素可以是不同类型。
Golang 中,数组可以这么用,基本上看下就明白
- var arr1 = [3]int{1, 2, 3}
- var arr2 = [...]string{"a", "b", "c"}
- var arr3 = [10]int{4: -1, 3: 5}
第三个声明表示,第 5 个元素,值是 - 1,第 4 个元素,值是 5,剩下的都是 0
注意,数组长度也是类型的一部分,也就是说[3]int
和[4]int
是不同的类型。数组的长度必须在编译时就可以确定,比如说是常量(不能是变量)
数组长度是不可变的。所以别想着 append 了,不行的?
切片 slice
如果想要一种长度可变的 “数组”,我们就需要 slice 了。Slice 这玩意才有点像 Python 的列表,只不过 Slice 的元素类型也要保持一致的。
那么如何创建 slice 呢?一般来说有这么几种做法:
- 从已有数组做切片,比如说
var s1 = arr2[:2]
需要注意的是,创建一个数组的 slice,就相当于创建了一个别名,改掉 slice,arr 也会变。 - 用 make
make([]int, 3, 40)
3 是 len,40 是容量。容量这个概念很容易混淆,这个切片难道塞满了 40 个元素就不能再塞吗?其实是能的,这个 40 只是为了方便内存分配和优化的。 - 用定义创建
var slice = []int{1, 2, 3}
,空 slice 可以var sli []int
Slice 的 copy
Slice 的 copy 很魔性,究竟有多魔性呢?我们正常以为的 copy,就是取而代之。但是 slice 的 copy 不是,copy 会把 dst 的元素依顺序复制到 src 中,覆盖 src 原有元素(如果长度够的话),剩下元素保持不变。
关于 slice 的容量,如果通过 make 函数创建 Slice 的时候指定了容量参数,那内存管理器会根据指定的容量的值先划分一块内存空间,然后才在其中存放有数组元素,多余部分处于空闲状态,在 Slice 上追加元素的时候,首先会放到这块空闲的内存中,如果添加的参数个数超过了容量值,内存管理器会重新划分一块容量值为原容量值 * 2 大小的内存空间,依次类推。这个机制的好处在能够提升运算性能,因为内存的重新划分会降低性能。
Slice 不可以做比较。
字典
Python 中键值对的结构被称作字典 dictionary,类似的数据类型还有 PHP 中的关联数组,实现方式为散列表,是一种用空间换时间的方便高效率查找的数据结构。
Go 中对应的数据结构叫做 map。
Python 字典的键类型必须是 hashable 的,但是类型可以不同;值的没有限制,是啥都行。
Go 中 map 的键值需要是同一类型的,我们如下定义,就意味着 key 是字符串,value 是整型数字
- var m1 = make(map[string]int)
或者直接这么用
- var m2 = map[string]int{
- "test1": 1,
- "test2": 2,
- }
可以这么访问m1["test"] = 3
,这个方法是安全的,不会报错。这里和 Python 的快速失败就不一样了。
如果 key 不在,那么会返回 value 的类型的默认值,对于 int 来说就是 0,对于 string 来说就是””
这就让人纠结了,真是的,如果要看 Key 在不在 map 中,就要多值返回,看第二个变量
- if _, ok := m2["key"]; ok {
- //存在
- }
遍历 map 可以这么玩
- for k, v := range m2 {
- fmt.Printf("%s -> %d\n", k, v)
- }
map 不可以用 == 做比较
结构体
和 C 语言的差不多,不过遵循大写导出的规则。在进行序列话、反序列化的时候要注意
- type Emp struct {
- ID int
- Name string
- }
这么用
- e1 := Emp{
- ID: 0,
- Name: "11a",
- }
结构体也可以嵌套,元素可以匿名(只有类型没有变量名)
JSON 序列化、反序列化
Python 中,将 object 转换成字符串的使用json.dumps
,称作序列化;将字符串转换为 object 的使用json.loads
,称作反序列化。另外,Python 还提供了两个类似的方法,分别为json.dump
和 json.load,只不过操作对象是文件。
Go 中,如果想要序列化、反序列化,对应的操作分别为json.Marshal()
和json.Unmarshal()
另外,struct 中只有大写的列才可以被序列化,也就是大写导出的原则
- type Message struct {
- Name string
- Body string
- Time int64
- }
- m := Message{"Alice", "Hello", 1294706395881547000}
- b, _ := json.Marshal(m)
- fmt.Println(b)
反序列化,需要先根据 json 的格式,定义好相应的 struct
- json_str := `{"Name":"Alice","Body":"Hello","Time":123}`
- j := []byte(json_str)
- var m Message
- err := json.Unmarshal(j, &m)
- fmt.Println(m)
但是序列化和反序列化,要求 struct 为大写,那到 json 里的 key 就大写了,有的时候我们不希望这样,这时可以使用 struct tags
- type Message struct {
- Name string `json:"name"` //自定义字段名称
- Body string `json:"body,omitempty"` //当值为空时,不序列化这个字段
- Time int64 `json:"-"` //跳过这个字段
- }
- m := Message{"Alice", "Hello", 1234}
- b, _ := json.Marshal(m)
- fmt.Println(string(b))
假如想反序列化任意数据,那么只能用接口了。毕竟是静态语言,不能在运行时才确定嘛。
- b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
- var f interface{}
- err := json.Unmarshal(b, &f)
- m := f.(map[string]interface{})
- k := m["Parents"].([]interface{})
- fmt.Println(m)
- fmt.Println(k[0])
同样的,如果要用文件进行序列化、反序列化操作,需要用Decoder
和Encoder
- type Post struct {
- Name string `json:"name"`
- Age int `json:"age"`
- }
- jsonFile, err := os.Open("post.json")
- if err != nil {
- fmt.Println("Error opening json file:", err)
- return
- }
- defer jsonFile.Close()
- decoder := json.NewDecoder(jsonFile)
- var post Post
- err = decoder.Decode(&post)
- fmt.Println(post)
参考阅读
https://sanyuesha.com/2018/05/07/go-json/
https://juejin.im/post/5d2eb9ae518825451f65f751
输入输出
Python 的 cli 输入输出用的是input
和print
,golang 用输出用的是fmt.Print
等系列,输入要复杂点,有这些方法
- reader := bufio.NewReader(os.Stdin)
- line, _, _ := reader.ReadLine()
- //或者ReadString
- text, _ := reader.ReadString('\n')
简单点直接用fmt.Scan
也可以。
读取单个字符可以用reader.ReadRune()
或者可以用 scanner
- scanner := bufio.NewScanner(os.Stdin)
- scanner.Scan()
- fmt.Println(scanner.Text())
哎妈呀太多了,反正我是记不住…… 估计是用的少吧
第三方扩展的使用
Python
在 Python 中,使用 pip 来安装标准库中没有的扩展,基本语法就是pip install package_name
,基本上不需要操心什么。就这么一句话,就完事,就够了,嗯,简单……
Golang
在 golang 中,使用 go get 来安装第三方扩展,但是情况要特殊一些。
Golang 依赖一个名为GOPATH
的环境变量,默认GOPATH
是~/go
。
- 未使用 go module
在未使用 go module 的情况下,go get 会把扩展下载到~/go/src
下
- 使用 go module
在 1.11 之后才开始支持 gomodule,同时需要设置GO111MODULE=on
或auto
在开启并使用 gomodule 的情况下,扩展会被下载到~/go/pkg/mod
下;
同时,你的项目需要一个go.mod
文件来指明需要哪些扩展。
如果使用 GoLand 的话,preferences-go-go modules 即可开启。
与 Python 类似,golang 的 import 查找路径也是系统标准库 -GOPATH/src
或pkg/mod
,当前工作目录。
使用的话
- go get https://github.com/360EntSecGroup-Skylar/excelize
- #如果需要特定版本
- go get https://github.com/360EntSecGroup-Skylar/excelize/v2
- go get https://github.com/360EntSecGroup-Skylar/excelize@v1.1.1
- #甚至可以特定commit
- go get https://github.com/360EntSecGroup-Skylar/excelize@v1.1.1@e3702bed2
前几天 GO 发布了 1.14,go module 终于被扶正了。
另外,对于中国程序员来说,弄个 Go Proxy 还是很有必要的,毕竟都是折翼天使啊
- export GOPROXY=https://goproxy.cn
包的概念
Python
Python 中,一个目录就是一个包,Python 3 的导入使用 absolute import,基本语法如下
- import os, platform
- import os as myos
- from os import *
- from os import uname, path as path2
Python 中,包的名字就是目录 + 文件名,按照层级依次导入。比较无脑,不用思考,就是和常识相符的
Golang
- import "fmt"
- import "os"
- import mybytes "bytes"
- import (
- "strings"
- "strconv"
- )
能被 import 的必须是可导出的,换句话说,首字母为大写
Golang 中,包的名字是由 package 关键字声明的,与文件名、路径名无关。
目录的名字只与 import 时相关,package 的名字与调用时有关。文件名与什么都无关。
同一个包的话,直接写里面大写的函数就能导入进来了,不需要傻乎乎的再import xxx
另外,注意下go run xxx.go
、go run a.go b.go
和go run .
哦
函数
由于 Python 是动态语言,所以函数参数可以不指定类型,返回值也可以不指定类型。想怎么返回就怎么返回,Type Annotation 也只是警告,实际上并没有强制报错的机制。
Golang 是静态语言,变量的类型要在编译时就确认,因此参数和返回值是要写在原型中的,错了直接编译都过不去。
最简单的示例
- func fun(p1, p2 int, p3 string) (string, bool) {
- fmt.Println(p1, p2, p3)
- return p3, true
- }
p1 和 p2 都是 int,p3 是 string,返回值为 string 和 bool。无参和无返回值自己脑补吧。另外返回值也可以带参数哦,直接空 return 就行了
- func fun(p1, p2 int) (r1, r2 int) {
- r1 = p1 * 10
- r2 = p2*10 + r1
- return
- }
golang 的函数参数既不支持默认参数,也不支持位置参数。但是可以用变长参数
- func fun(p1 int, p2 ...int)
调用时形如fun(1, 2, 3, 4, 5)
,2345 都是 p2 的,p2 是个 int 的 slice
Golang 的参数都是传值的。
数字、字符串、数组、struct 是非引用类型,所以函数中修改不会把原有值给改掉;指针、slice、map、chan 是引用类型的,你改了,那就跟着变了。
为了避免 slice 和 map 的这个特性,只能想办法做浅拷贝或深拷贝了。当然最好还是避免吧
异常
Python
Python 的异常机制就是大家都会的try…except…finally
,可能有的时候还需要 else,里面配合着 if-else, for-else,while-else,简直完美啊哈哈哈?。这个很简单,人人都会……
Golang
可惜的是,golang 中并没有如此明确的异常捕获机制。
在 golang 中,有很多函数调用都会返回两个值,第一个是期待的结果,第二个是err
,如果err==nil
,则说明没有发生错误。
类似的,golang 中有一个defer
、panic
、recover
,倒是有点像传统上的try…except…finally
。
go 的 defer 语句是用来延迟执行函数的,而且延迟发生在调用函数 return 之后,比如
- func a() int {
- defer b()
- return 0
- }
return 0
执行完了之后,b 才执行。
defer
一般用于清理资源,功能上 Python 里的with
recover
则更像是 Python 中的except
,panic
则是raise
- func fun() {
- if ok := recover(); ok != nil {
- fmt.Println("recover")
- }
- }
- func main() {
- defer fun()
- panic("error")
- }
多个 defer,defer 从下到上执行。
这玩意到底咋用,反正记住了,defer 是这个函数执行完了,再走的它。recover 举一个例子
Panic 就不说了。
并发
Python 中如果要进行并发操作,我们一般会根据情况应用多进程、多线程,甚至会用上 celery 这类。Golang 中,我们用 go routine,用起来很简单,函数前加一个 go 关键字就可以了。
- func compute(value int) {
- for i := 0; i < value; i++ {
- time.Sleep(time.Second)
- fmt.Println(i)
- }
- }
- func main() {
- fmt.Println("Goroutine Tutorial")
- go compute(3)
- go compute(3)
- fmt.Scanln()
- }
当然,并发之后的这些 goroutine 如何控制,那就是 context 的事情了。咱也暂时不明白
通道 channel
如果 goroutine 想要通信,那么就要用 channel 啦。
- func CalculateValue(values chan int) {
- value := rand.Intn(10)
- fmt.Println("Calculated Random Value: {}", value)
- values <- value
- }
- func main() {
- fmt.Println("Go Channel Tutorial")
- values := make(chan int)
- defer close(values)
- go CalculateValue(values)
- value := <-values
- fmt.Println(value)
- }
总结下
- // 创建int类型的通道
- myChannel := make(chan int)
- //把通道作为参数调用
- go CalculateValue(myChannel)
- // 把value变量的值送给chan
- channel <- value
- //取值,复制给value变量
- value := <- channel
需要注意的是,这种 channel 是阻塞的,被称作 “无缓冲通道”。解释一下,默认的无缓冲通道,一个 go routine 向通道中发送了数据,那么这个 goroutine 就会被阻塞,下面的代码都会等待执行,直到另外一个 goroutine 从通道中取值为止。同理,如果一方先收了,那么也会阻塞直到另外的 goroutine 发送,因此无缓冲通道也可以称作同步通道。
那咋办啊,假如还想射后不理,那就需要用到缓冲通道。
- bufferedChannel := make(chan int, 3)
满了之后,当然就是继续阻塞啦。
巧妙的,我利用了通道的阻塞的这个特性,为 WebP Server 增加 Prefetch 使用 CPU 核心数的限制,注意看 23、24 和 42 行,也不知道这算不算是正经的用法
接口
Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
反正我是没怎么看懂……
- package main
- import "fmt"
- // 实现一个鸭子
- type Duck interface {
- Swim() // 游泳
- Feathers() // 羽毛
- }
- // 实现一个会叫的鸭子,因为嵌入了Duck,所以也有swim和feathers方法
- type QuackDuck interface {
- Quack() // 嘎嘎叫
- Duck // 嵌入接口
- }
- //真实鸭子的实现
- type RealDuck struct{}
- func (RealDuck) Swim() {
- fmt.Println("用鸭璞向后划水")
- }
- func (RealDuck) Feathers() {
- fmt.Println("遇到水也不会湿的羽毛")
- }
- func (RealDuck) Quack() {
- fmt.Println("嘎~ 嘎~ 嘎~")
- }
- // 玩具鸭的实现
- type ToyDuck struct{}
- func (ToyDuck) Swim() {
- fmt.Println("以固定的速度向前移动")
- }
- func (ToyDuck) Feathers() {
- fmt.Println("白色的固定的塑料羽毛")
- }
- func main() {
- var duck1 Duck
- duck1 = RealDuck{}
- duck1.Swim()
- duck1.Feathers()
- //duck1.Quack() //报错,Duck没有Quack方法
- var duck2 Duck
- duck2 = ToyDuck{}
- duck2.Swim()
- duck2.Feathers()
- //duck2.Quack() //报错,Duck没有Quack方法
- var duck3 QuackDuck
- duck3 = RealDuck{}
- duck3.Swim()
- duck3.Feathers()
- duck3.Quack()
- var duck4 QuackDuck
- duck4 = ToyDuck{} //类型错误
- duck4.Swim()
- duck4.Feathers()
- duck4.Quack()
- }
https://blog.biezhi.me/2019/01/learn-golang-interfaces.html
OOP
Golang 其实并不是那么严格的面向对象的语言,不过 struct 中嵌入其他 struct,就很接近 class 这个概念了。准确的来说,这种特性是组合,而不是继承。
定义方法时,格式是这样的 func (结构体名) 方法名 {},比如func (c Cat) sleep()
- package main
- import (
- "fmt"
- "strconv"
- )
- // 动物类
- type Animal struct {
- name string
- subject string
- }
- // 动物的公共方法
- func (a *Animal) eat(food string) {
- fmt.Println(a.name + "喜欢吃:" + food + ",它属于:" + a.subject)
- }
- // 猫类,继承动物类
- type Cat struct {
- // 继承动物的属性和方法
- Animal
- // 猫自己的属性
- age int
- }
- // 猫类独有的方法
- func (c Cat) sleep() {
- fmt.Println(c.name + " 今年" + strconv.Itoa(c.age) + "岁了,特别喜欢睡觉")
- }
- func main() {
- // 创建一个动物类
- animal := Animal{name: "动物", subject: "动物科"}
- animal.eat("肉")
- // 创建一个猫类
- cat := Cat{Animal: Animal{name: "咪咪", subject: "猫科"}, age: 1}
- cat.eat("鱼")
- cat.sleep()
- }
反正我也没咋弄明白…… 参考阅读
https://learnku.com/articles/32295
文件操作
Python 中,对文件进行操作,使用 open 打开,配合不同的操作模式,然后使用 read、write 方法进行读写。
Golang 中,对文件操作最简单的可以使用 io/ioutil
- //读:
- data, _ := ioutil.ReadFile("test.txt")
- //写:
- content := []byte("hello word 你好真好")
- err := ioutil.WriteFile("test.txt", content, 0755)
- if err != nil {
- fmt.Println(err)
- }
文件不存在,会创建;存在,覆盖。
如果我们想追加的话,可以这样
- f, _ := os.OpenFile("test.txt", os.O_APPEND|os.O_WRONLY, 0777)
- f.WriteString("hello1234")
- f.Close()
第二个参数指定文件模式。想要一行一行的读,那么用 bufio 里的 Read 系列(ReadBytes
、ReadString
和 ReadLine
)就可以了,不过要特别注意换行符的问题
- f, _ := os.Open("test.txt")
- b:=bufio.NewReader(f)
- for {
- a, _, c := b.ReadLine()
- if c == io.EOF {
- break
- }
- fmt.Println(string(a))
- }
当然了, 有一个非常严重的问题:golang 假定所读的文件都是 UTF-8 编码的,所以在 windows 下如果这么读一个记事本创建的文本文档,那自然就乱码了。
需要这几个库
golang.org/x/text/transform
golang.org/x/text/encoding/simplifiedchines
可以用transform.NewReader
进行操作
- f, _ := os.Open("test.txt")
- r := transform.NewReader(f, simplifiedchinese.GBK.NewDecoder())
- d, _ := ioutil.ReadAll(r)
- fmt.Println(string(d))
也可以这样先读成 bytes,然后再转
- b, _ := ioutil.ReadFile("test.txt")
- reader := transform.NewReader(bytes.NewReader(b), simplifiedchinese.GBK.NewDecoder())
- d, _ := ioutil.ReadAll(reader)
- fmt.Println(string(d))
目录操作
Python 中,目录操作,包括创建,重命名,改变权限等可以用 os 模块,路径相关操作可以用os.path
或者是pathlib
。
Golang 中,对目录操作,基本上也可以应用 os 模块。
对路径操作,基本上用path/filepath
就差不多了
基本上看到就能猜到什么意思。
Dir
获取目录名,base
获取文件名,Join
拼接路径,filepath.Separator
是换行符,Split
把路径分割成文件和目录,Clean
规范化路径,去掉多余的 / 什么的
咋获取当前目录啊?os.Args
里就有啦。filepath.Abs(filepath.Dir(os.Args[0]))
就成。
时间日期标准库
Python 中,如果要进行时间相关的操作,一般会应用 time 标准库。基本上常用的操作就是字符串 - 时间互相转换,获取当前时间戳,把时间戳转换成日期,把日期转换为时间戳。
Golang 中也有一个 time 库,基本用法总结下
- //显示当前时间戳
- time.Now().Unix()
- //时间戳转换成time结构
- time.Unix(10,0)
- //字符串转日期,需要注意Parse在缺少时区信息时,会默认0时区,所以会差8小时
- time.Parse("2006-01-02 15:04:05", "2020-02-01 14:08:00")
- //要想不差8小时,那咱就
- time.ParseInLocation("2006-01-02 15:04:05", "2020-02-01 14:08:00",time.Local)
这就没问题了。2006-01-02 15:04:05
是固定格式,类似传统的 ymdhms,记不住啊记不住啊!
日期转换为字符串
- t.Format("今天是2006年")
附带功能
time.Now()
附带了After
、Before
、Sub
、Add
等方法,方便时间运算
执行系统命令
Python 里这个方法很多,os.system()
,subprocess
等等。Golang 有一个exec
- out, _ := exec.Command("ls").Output()
- fmt.Println(string(out))
Web 框架
在用 Python 的时候,我一般会选择 tornado 作为 web 框架,主要原因就是 tornado 的性能比较好,支持异步非阻塞,并且可以很方便的通过 self 来进行各种操作。Golang 的 web 框架也很多,比如说 beego,echo,gin。
当然了,golang 自带的net/http
模块本身本身提供了很多功能,我们可以应用它创建一个 web 服务器,如下代码即可
- func IndexHandler(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintln(w, "hello world")
- }
- func main() {
- http.HandleFunc("/", IndexHandler)
- err := http.ListenAndServe("127.0.0.1:8000", nil)
- fmt.Println(err)
- }
经过一些纠结,我选择了 gin……
基础用法
- package main
- import "github.com/gin-gonic/gin"
- func main() {
- r := gin.Default()
- r.GET("/ping", func(c *gin.Context) {
- c.JSON(200, gin.H{
- "message": "pong",
- })
- })
- r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
- }
这里 r.GET 就是路由,后面这里用了匿名函数,当然不想用匿名函数也成。
如果要支持其他 http 方法,那就这样
- r.POST("/ping",ping)
返回数据
c.String, c.JSON等
获取 url 参数
- Get 参数
- c.Query(key)
- post 参数
- c.PostForm("name")
- 上传文件
- file, _ := c.FormFile("file")
- err := c.SaveUploadedFile(file, file.Filename)
- 获取 raw body
- c.GetRawData()
- 静态文件
- router.Static("/assets", "./assets")
- router.StaticFS("/more_static", http.Dir("my_file_system"))
- router.StaticFile("/favicon.ico", "./resources/favicon.ico")
- http 重定向
- c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
- 路由重定向
- c.Request.URL.Path = "/test2"
- r.HandleContext(c)
- cookie
- cookie, err := c.Cookie("gin_cookie")
- c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", http.SameSiteLaxMode, false, true)
Requests
Python 中,我们一般使用 requests 获取 http 资源,当然用 urllib 也可以,只不过稍微有些繁琐
同理,golang 的 net/http 库已经提供了最基础的操作。
- resp, _ := http.Get("http://example.com/")
- data, _ := ioutil.ReadAll(resp.Body)
- fmt.Println(string(data))
- resp.Body.Close()
啊对了,如果返回的是 json 的话,resp.Body
可以直接被Unmarshal
的哦
还有一个人封装的 grequests,用起来也不错
- //发get
- resp, err := grequests.Get("http://httpbin.org/get", nil)
- if err != nil {
- log.Fatalln("Unable to make request: ", err)
- }
- fmt.Println(resp.String())
- //发post
- d := grequests.RequestOptions{
- Data: map[string]string{"hello": "world"},
- }
- resp, err := grequests.Post("http://httpbin.org/post", &d)
- if err != nil {
- log.Fatalln("Unable to make request: ", err)
- }
- fmt.Println(resp.String())
文档 https://pkg.go.dev/github.com/levigross/grequests
MySQL
Python 连接 MySQL 我们一般用 pymysql,建立连接,获取游标,执行查询。Golang 也基本差不多,需要提前安装这个模块go get -u github.com/go-sql-driver/mysql
- import (
- "database/sql"
- "fmt"
- _ "github.com/go-sql-driver/mysql"
- )
建立连接,典型的连接是这样的,当然有些参数是可以省略的。
- db, _ := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test?charset=utf8mb4")
- defer db.Close()
- //插入,直接这样参数化查询,返回一个result,一个error
- db.Exec("INSERT INTO user VALUES (?,?)", "Benny", 18)
- //或者下面这这样用prepare,没啥区别
- stmt, _ := db.Prepare("INSERT INTO user VALUES (?,?)")
- stmt.Exec("Amy", 19)
- // 删除,可以这么玩
- db.Exec("DELETE FROM user WHERE name=?", "Amy")
- // 修改,我猜你已经猜到了吧
- db.Exec("UPDATE user SET age=? WHERE name=?", 19, "Benny")
- // 查询,Query用于返回结果集,上面的Exec就不能在这里用了,可以想象成Python的execute之后再fetch
- data, _ := db.Query("select version();")
- for data.Next() {
- var v string
- data.Scan(&v)
- fmt.Println(v)
- }
- // 再举一个例子?,查询一个表的数据,这个时候我们如果定义struct就更好了是吧
- type User struct {
- name string
- age int
- }
- data, _ := db.Query("SELECT * FROM user")
- var s []User
- for data.Next() {
- var user User
- data.Scan(&user.name, &user.age)
- s = append(s, user)
- }
- fmt.Println(s)
插入的时候想要用类似 pymysql 的executemany
?不好意思,得自己实现了
如果需要事务,那么可以这样
- tx, _ := db.Begin()
- tx.Exec("INSERT INTO image VALUES (?,?)", "abc", "def")
- tx.Commit()
- tx.Rollback()
MongoDB
MongoDB 官方维护了一个 mongo-go-driver,使用go get go.mongodb.org/mongo-driver
即可安装
Golang 毕竟还是静态类型的语言,因此用 mongodb 还是有点折磨人的?
连接数据库
用起来,这样就建立了到数据库的连接
- ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)
- client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
- if err != nil {
- fmt.Println(err)
- }
- defer client.Disconnect(ctx)
这个 URI,实际上就是 mongodb 的连接字符串,定义如下
- mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[database][?options]]
举例如下,如果数据库带认证,那么就要选择 2 和 3 了。
- mongodb://mongodb0.example.com:27017/admin
- mongodb://myDBReader:D1fficultP%40ssw0rd@mongodb0.example.com:27017/admin
- mongodb://user:password@example.com/?authSource=the_database&authMechanism=SCRAM-SHA-1"
连接成功之后,我们需要选择数据库,选择集合
- db:=client.Database("go")
- accountCol:=db.Collection("account")
bson
之后这个 col 就可以让我们操作数据库了,但是我们需要提前熟悉一下 golang 中的 bson
总共有 4 种类型,分别为 DMAE
D 表示 BSON 文档,有序 kv
M 与 D 相同,只是无序
A 数组
E 单个文档,只能有一对 kv
- d := bson.D{
- {"Name", "mark"},
- {"Age", 12},
- }
- fmt.Println(d)
- // bson.M是一个map 无序的key-value集合,在不要求顺序的情况下可以替代bson.D
- m := bson.M{
- "name": "mark",
- "age": 12,
- }
- fmt.Println(m)
- // bson.A 是一个数组
- a := bson.A{
- "jack", "rose", "jobs",
- }
- fmt.Println(a)
- // bson.E是只能包含一个key-value的map
- e := bson.E{
- "name", "mark",
- }
- fmt.Println(e)
查询
查找一个,用FindOne
就够了,第二个参数是查询条件,当然了我们要 decode 给一个定义好的变量
- var result Account
- col.FindOne(ctx, bson.D{}).Decode(&result)
- fmt.Println(result)
- col.FindOne(ctx, bson.D{{"name", "Benny",}}).Decode(&result)
查询多个
- var results []Account
- cur, _ := col.Find(ctx, bson.D{})
- for cur.Next(ctx) {
- var result Account
- cur.Decode(&result)
- results = append(results, result)
- }
- cur.Close(ctx)
- fmt.Println(results)
插入
- var data=Account{
- Name: "Sally",
- Age: 12,
- Gender: "female",
- }
- col.InsertOne(ctx,data)
插入多个
- var data = []interface{}{
- Account{
- Name: "Benny2",
- Age: 10,
- Gender: "1",
- },
- Account{
- Name: "Benny3",
- Age: 40,
- Gender: "12",
- },
- }
- col.InsertMany(ctx, data)
修改
- col.UpdateOne(ctx,
- bson.M{"name": "Benny"},
- bson.M{"$set": bson.M{"name": "Benny!"}},
- )
- col.UpdateMany(ctx,
- bson.M{"name": "Benny!"},
- bson.M{"$set": bson.M{"age": 14}},
- )
替换
- col.ReplaceOne(ctx,
- bson.M{"name": "Sally"},
- bson.D{{"name2", "123d"},
- {"job", "coder"},
- },
- )
删除
- col.DeleteOne(ctx,
- bson.M{"name": "Benny!"},
- )
- col.DeleteMany(ctx,
- bson.M{"name": "Benny!"},
- )
其他操作
基本上也都是一个套路了
- db.ListCollectionNames(ctx, bson.M{})
- client.ListDatabases(ctx,bson.M{})
- col.Drop(ctx)
https://www.mongodb.com/blog/post/mongodb-go-driver-tutorial
正则表达式
作为一名不太会写、会用正则表达式的小豆子,我一般都是无脑的 findall 的,有时也会使用 sub
Golang 中使用正则,emmm 我也不太会啊……
- text := "hello 9123 世界"
- re := regexp.MustCompile(`\w`)
- result := re.FindAllString(text,-1)
- fmt.Println(strings.Join(result,""))
Base64
- text := "hello"
- s := base64.StdEncoding.EncodeToString([]byte(text))
- fmt.Println(s)
- r, _ := base64.StdEncoding.DecodeString(s)
- fmt.Println(string(r))
读写 Excel
Python 中读写 Excel 一般使用 xlrd/xlwt 或者 openpyxl 库,基本操作思路是打开 - 选择 sheet - 操作单元格。Golang 中有一个 excelize,也可以进行类似的操作。注意,我一般选择 v2 的那个版本,所以 go get 的时候要小心哦
- go get https://github.com/360EntSecGroup-Skylar/excelize/v2
读
- f, err := excelize.OpenFile("sample.xlsx")
- if err != nil {
- fmt.Println(err)
- return
- }
- cell := f.GetCellValue("Sheet1", "A1")
- //如果想要遍历的话,
- rows,_ := f.GetRows("Sheet1")
- for _, row := range rows {
- for _, colCell := range row {
- fmt.Print(colCell, "\t")
- }
- fmt.Println()
- }
创建 Excel
- f:=excelize.NewFile()
- f.SetCellValue("sheet1","A1","hello123")
- f.SaveAs("hello.xlsx")
编辑 Excel
- f, _ := excelize.OpenFile("sample.xlsx")
- f.SetCellValue("sheet1", "A4", "edited!")
- f.Save()
其他辅助工具
使用过 xlrd 和 openpyxl 的盆友们可能知道,他们一个用 A1A2 这样的坐标,另外一个用 0,0,1,1 这种索引,别怕盆友们
- x, y, _ := excelize.CellNameToCoordinates("A1")
- name, _ := excelize.CoordinatesToCellName(3, 4)
想要转一列 也可以
- excelize.ColumnNameToNumber("B")
- excelize.ColumnNumberToName(5)
啥?完事了
啊对不住各位亲朋好友们,到这里就没了,那个基础语法靠自己了…… 标准库和常用库也靠自己了,或者等我慢慢更新吧
这篇笔记总结起来大概花了 500 分钟,希望能够帮助到有类似需求、有着类似的学习曲线的人。喜欢的嘛麻烦给我的项目 WebP Server Go 点个 star~谢谢亲们?❤️