之前在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~谢谢亲们?❤️