土豆不好吃

Python程序员的Golang学习笔记

文章目录[显示]
这篇文章在 2023年11月08日09:01:09 更新了哦~

之前在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呢?一般来说有这么几种做法:

  1. 从已有数组做切片,比如说var s1 = arr2[:2] 需要注意的是,创建一个数组的slice,就相当于创建了一个别名,改掉slice,arr也会变。
  2. 用make make([]int, 3, 40) 3是len,40是容量。容量这个概念很容易混淆,这个切片难道塞满了40个元素就不能再塞吗?其实是能的,这个40只是为了方便内存分配和优化的。
  3. 用定义创建 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])

同样的,如果要用文件进行序列化、反序列化操作,需要用DecoderEncoder

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输入输出用的是inputprint,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 get会把扩展下载到~/go/src

在1.11之后才开始支持gomodule,同时需要设置GO111MODULE=onauto

在开启并使用gomodule的情况下,扩展会被下载到~/go/pkg/mod下;

同时,你的项目需要一个go.mod文件来指明需要哪些扩展。

如果使用GoLand的话,preferences-go-go modules即可开启。

与Python类似,golang的import查找路径也是系统标准库-GOPATH/srcpkg/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.gogo run a.go b.gogo 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中有一个deferpanicrecover,倒是有点像传统上的try…except…finally

go 的 defer 语句是用来延迟执行函数的,而且延迟发生在调用函数 return 之后,比如

func a() int {
  defer b()
  return 0
}

return 0执行完了之后,b才执行。

defer一般用于清理资源,功能上Python里的with

recover则更像是Python中的exceptpanic则是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系列(ReadBytesReadStringReadLine)就可以了,不过要特别注意换行符的问题

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()附带了AfterBeforeSubAdd等方法,方便时间运算

执行系统命令

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参数

c.Query(key)
c.PostForm("name")
file, _ := c.FormFile("file")
err := c.SaveUploadedFile(file, file.Filename)
c.GetRawData()
router.Static("/assets", "./assets")
router.StaticFS("/more_static", http.Dir("my_file_system"))
router.StaticFile("/favicon.ico", "./resources/favicon.ico")
c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
c.Request.URL.Path = "/test2"
r.HandleContext(c)
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~谢谢亲们?❤️


文章版权归原作者所有丨本站默认采用CC-BY-NC-SA 4.0协议进行授权|
转载必须包含本声明,并以超链接形式注明原作者和本文原始地址:
https://dmesg.app/from-pyhon-to-go.html
退出移动版