登录
  • 人们都希望被别人需要 却往往事与愿违
  • 投资是预测资产收益的活动, 而投机是预测市场心理的活动 @凯恩斯

Python 程序员的 Golang 学习笔记

编程 Benny 小土豆 8172 次浏览 19612 字 2 个评论
文章目录 [显示]
这篇文章在 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,还记得吧)

观察以下代码

  1. var eng = "abc"
  2. var chs = "你好吗"
  3.  
  4. fmt.Println(len(eng))
  5. fmt.Println(len(chs))
  6.  

输出分别为 3 和 9。

不同于 Python,python 中这两者返回值都会是 3,因为一共就 3 个字符嘛,管你中文英文的;golang 中len表示字节数,由于在 UTF-8 中文使用三子节表示,所以三个汉字对应为 9。

如要实现类似 Python 中的 len,那么需要使用utf8.RuneCountInString(eng),计算实际字符数量。不过遇到了 👨‍👩‍👧‍👦 这种四个 emoji+3 个零宽字符时结果是 7

由于这一点的不同,在遍历时也会有类似的问题。一个需要注意的事情就是,遍历的核心是要准确的 “切割” 每个字,所以在chs这个字符串中,要遍历三次。

遍历字符串,用 range 循环的话

  1. for k, v := range chs {
  2. fmt.Println( k, v)
  3. }

循环会进行 3 次,因为一共时三个字;k 表示索引,v 的值实际上是 “一串数字 “,而不是我们预期的单个字符 “你”。可以用 string 转换,或者%c量词

用下标的话,按照常规的想法,我们会这样做

  1. for i := 0; i < len(chs); i++ {
  2. fmt.Println( chs[i])
  3. }

❌❌❌实际上循环会进行 9 次,这就错了

正确的姿势应该是转换为 rune 数组

  1. var c = []rune(chs)
  2. for i := 0; i < len(c); i++ {
  3. fmt.Println(c[i])
  4. }

这样就是三次循环了。或者这样做切片

  1. for i := 0; i < len(chs); {
  2. r, size := utf8.DecodeRuneInString(chs[i:])
  3. i += size
  4. fmt.Println(string(r))
  5. }

DecodeRuneInString会把字符串解码成 “数字”。

是不是觉得很懵圈,这是什么垃圾玩意!哎,反正用range就没毛病了。

如果要取第一个字 “你”,用chs[0]是错误的,用chs[0]实际上访问的是前 8 个字节,对于 ASCII 字符来说自然没问题,但是其他字符就有问题了。

所以要取第一个字符咋办啊?转换为 rune 数组啊

  1. var c = []rune(chs)
  2. fmt.Println(c[0])

如果要取最后一个字符,不可以像 Python 一样用 - 1,要用c[len(c)-1]

参考阅读 《从 golang 字符串 string 遍历说起:聊聊 go 语言的 Strings、bytes、runes 和字符》

字符串常见操作,如搜索、替换、比较基本上都包含在了 strings 包中;

类型转换,包含在strconv中,如:

Atoi 字符串转数字,Itoa数字转字符串

或者更通用,用 Format 数字转字符串、Parse 字符串转数字类型,举例如下,注意返回两个制,第二个是 err

  1. fmt.Println(strconv.ParseInt("4567",10,0))
  2. fmt.Println(strconv.ParseInt("10",2,0))
  3. fmt.Println(strconv.FormatInt(1234,10))
  4.  

第二个参数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,定义多个常量可以这样

  1. const (
  2. a = 1
  3. b
  4. c = 3
  5. d
  6. )

b 和 d 留空,意味着会取它上面的那个常量的值,也就是 1 和 3

使用常量生成器 iota 可以生成相关值的常量,如下代码

  1. const (
  2. a = iota
  3. b
  4. c
  5. d
  6. )

值分别为 0123

甚至还可以这么玩,这就是有点神仙操作了

  1. const (
  2. _ = 1 << (iota * 10)
  3. KiB
  4. MiB
  5. GiB
  6. )

顺便说一句,定义变量的时候,a:=3 var a=3, var a int的区别咱就不说了。

复合数据类型

数组

Python 中,其实也是有 array 这种类型的,类型都是固定的,而且还可变

  1. from array import array
  2.  
  3. a = array('l', [1, 2, 3, 4, 5])
  4. a.append(5)
  5. a[3] = 999

在上述代码中,每一个元素都要是整型。一般来说,我们会用 list 来表示数组,只不过 list 的元素可以是不同类型。

Golang 中,数组可以这么用,基本上看下就明白

  1. var arr1 = [3]int{1, 2, 3}
  2. var arr2 = [...]string{"a", "b", "c"}
  3. 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 是整型数字

  1. var m1 = make(map[string]int)

或者直接这么用

  1. var m2 = map[string]int{
  2. "test1": 1,
  3. "test2": 2,
  4. }

可以这么访问m1["test"] = 3,这个方法是安全的,不会报错。这里和 Python 的快速失败就不一样了。

如果 key 不在,那么会返回 value 的类型的默认值,对于 int 来说就是 0,对于 string 来说就是””

这就让人纠结了,真是的,如果要看 Key 在不在 map 中,就要多值返回,看第二个变量

  1. if _, ok := m2["key"]; ok {
  2. //存在
  3. }

遍历 map 可以这么玩

  1. for k, v := range m2 {
  2. fmt.Printf("%s -> %d\n", k, v)
  3. }

map 不可以用 == 做比较

结构体

和 C 语言的差不多,不过遵循大写导出的规则。在进行序列话、反序列化的时候要注意

  1. type Emp struct {
  2. ID int
  3. Name string
  4. }

这么用

  1. e1 := Emp{
  2. ID: 0,
  3. Name: "11a",
  4. }

结构体也可以嵌套,元素可以匿名(只有类型没有变量名)

JSON 序列化、反序列化

Python 中,将 object 转换成字符串的使用json.dumps,称作序列化;将字符串转换为 object 的使用json.loads,称作反序列化。另外,Python 还提供了两个类似的方法,分别为json.dump和 json.load,只不过操作对象是文件。

Go 中,如果想要序列化、反序列化,对应的操作分别为json.Marshal()json.Unmarshal()

另外,struct 中只有大写的列才可以被序列化,也就是大写导出的原则

  1. type Message struct {
  2. Name string
  3. Body string
  4. Time int64
  5. }
  6. m := Message{"Alice", "Hello", 1294706395881547000}
  7. b, _ := json.Marshal(m)
  8. fmt.Println(b)

反序列化,需要先根据 json 的格式,定义好相应的 struct

  1. json_str := `{"Name":"Alice","Body":"Hello","Time":123}`
  2. j := []byte(json_str)
  3. var m Message
  4. err := json.Unmarshal(j, &m)
  5. fmt.Println(m)

但是序列化和反序列化,要求 struct 为大写,那到 json 里的 key 就大写了,有的时候我们不希望这样,这时可以使用 struct tags

  1. type Message struct {
  2. Name string `json:"name"` //自定义字段名称
  3. Body string `json:"body,omitempty"` //当值为空时,不序列化这个字段
  4. Time int64 `json:"-"` //跳过这个字段
  5. }
  6.  
  7. m := Message{"Alice", "Hello", 1234}
  8. b, _ := json.Marshal(m)
  9. fmt.Println(string(b))

假如想反序列化任意数据,那么只能用接口了。毕竟是静态语言,不能在运行时才确定嘛。

  1. b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
  2. var f interface{}
  3. err := json.Unmarshal(b, &f)
  4. m := f.(map[string]interface{})
  5. k := m["Parents"].([]interface{})
  6. fmt.Println(m)
  7. fmt.Println(k[0])

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

  1. type Post struct {
  2. Name string `json:"name"`
  3. Age int `json:"age"`
  4. }
  5. jsonFile, err := os.Open("post.json")
  6. if err != nil {
  7. fmt.Println("Error opening json file:", err)
  8. return
  9. }
  10.  
  11. defer jsonFile.Close()
  12. decoder := json.NewDecoder(jsonFile)
  13.  
  14. var post Post
  15. err = decoder.Decode(&post)
  16.  
  17. fmt.Println(post)

参考阅读

https://sanyuesha.com/2018/05/07/go-json/

https://juejin.im/post/5d2eb9ae518825451f65f751

输入输出

Python 的 cli 输入输出用的是inputprint,golang 用输出用的是fmt.Print等系列,输入要复杂点,有这些方法

  1. reader := bufio.NewReader(os.Stdin)
  2. line, _, _ := reader.ReadLine()
  3. //或者ReadString
  4. text, _ := reader.ReadString('\n')

简单点直接用fmt.Scan也可以。

读取单个字符可以用reader.ReadRune()

或者可以用 scanner

  1. scanner := bufio.NewScanner(os.Stdin)
  2. scanner.Scan()
  3. 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=onauto

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

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

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

与 Python 类似,golang 的 import 查找路径也是系统标准库 -GOPATH/srcpkg/mod,当前工作目录。

使用的话

  1. go get https://github.com/360EntSecGroup-Skylar/excelize
  2.  
  3. #如果需要特定版本
  4. go get https://github.com/360EntSecGroup-Skylar/excelize/v2
  5. go get https://github.com/360EntSecGroup-Skylar/excelize@v1.1.1
  6.  
  7. #甚至可以特定commit
  8. go get https://github.com/360EntSecGroup-Skylar/excelize@v1.1.1@e3702bed2

前几天 GO 发布了 1.14,go module 终于被扶正了。

另外,对于中国程序员来说,弄个 Go Proxy 还是很有必要的,毕竟都是折翼天使啊

  1. export GOPROXY=https://goproxy.cn

包的概念

Python

Python 中,一个目录就是一个包,Python 3 的导入使用 absolute import,基本语法如下

  1. import os, platform
  2. import os as myos
  3. from os import *
  4. from os import uname, path as path2

Python 中,包的名字就是目录 + 文件名,按照层级依次导入。比较无脑,不用思考,就是和常识相符的

Golang

  1. import "fmt"
  2. import "os"
  3. import mybytes "bytes"
  4.  
  5. import (
  6. "strings"
  7. "strconv"
  8. )

能被 import 的必须是可导出的,换句话说,首字母为大写

Golang 中,包的名字是由 package 关键字声明的,与文件名、路径名无关。

目录的名字只与 import 时相关,package 的名字与调用时有关。文件名与什么都无关。

同一个包的话,直接写里面大写的函数就能导入进来了,不需要傻乎乎的再import xxx

另外,注意下go run xxx.gogo run a.go b.gogo run .

函数

由于 Python 是动态语言,所以函数参数可以不指定类型,返回值也可以不指定类型。想怎么返回就怎么返回,Type Annotation 也只是警告,实际上并没有强制报错的机制。

Golang 是静态语言,变量的类型要在编译时就确认,因此参数和返回值是要写在原型中的,错了直接编译都过不去。

最简单的示例

  1. func fun(p1, p2 int, p3 string) (string, bool) {
  2. fmt.Println(p1, p2, p3)
  3. return p3, true
  4. }

p1 和 p2 都是 int,p3 是 string,返回值为 string 和 bool。无参和无返回值自己脑补吧。另外返回值也可以带参数哦,直接空 return 就行了

  1. func fun(p1, p2 int) (r1, r2 int) {
  2. r1 = p1 * 10
  3. r2 = p2*10 + r1
  4. return
  5. }

golang 的函数参数既不支持默认参数,也不支持位置参数。但是可以用变长参数

  1. 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 之后,比如

  1. func a() int {
  2. defer b()
  3. return 0
  4. }

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

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

recover则更像是 Python 中的exceptpanic则是raise

  1. func fun() {
  2. if ok := recover(); ok != nil {
  3. fmt.Println("recover")
  4. }
  5. }
  6.  
  7. func main() {
  8. defer fun()
  9. panic("error")
  10. }

多个 defer,defer 从下到上执行。

这玩意到底咋用,反正记住了,defer 是这个函数执行完了,再走的它。recover 举一个例子

Python程序员的Golang学习笔记

Panic 就不说了。

并发

Python 中如果要进行并发操作,我们一般会根据情况应用多进程、多线程,甚至会用上 celery 这类。Golang 中,我们用 go routine,用起来很简单,函数前加一个 go 关键字就可以了。

  1. func compute(value int) {
  2. for i := 0; i < value; i++ {
  3. time.Sleep(time.Second)
  4. fmt.Println(i)
  5. }
  6. }
  7.  
  8. func main() {
  9. fmt.Println("Goroutine Tutorial")
  10.  
  11. go compute(3)
  12. go compute(3)
  13.  
  14. fmt.Scanln()
  15. }

当然,并发之后的这些 goroutine 如何控制,那就是 context 的事情了。咱也暂时不明白

通道 channel

如果 goroutine 想要通信,那么就要用 channel 啦。

  1. func CalculateValue(values chan int) {
  2. value := rand.Intn(10)
  3. fmt.Println("Calculated Random Value: {}", value)
  4. values <- value
  5. }
  6.  
  7. func main() {
  8. fmt.Println("Go Channel Tutorial")
  9.  
  10. values := make(chan int)
  11. defer close(values)
  12.  
  13. go CalculateValue(values)
  14.  
  15. value := <-values
  16. fmt.Println(value)
  17. }

总结下

  1. // 创建int类型的通道
  2. myChannel := make(chan int)
  3.  
  4. //把通道作为参数调用
  5. go CalculateValue(myChannel)
  6.  
  7. // 把value变量的值送给chan
  8. channel <- value
  9.  
  10. //取值,复制给value变量
  11. value := <- channel
  12.  

需要注意的是,这种 channel 是阻塞的,被称作 “无缓冲通道”。解释一下,默认的无缓冲通道,一个 go routine 向通道中发送了数据,那么这个 goroutine 就会被阻塞,下面的代码都会等待执行,直到另外一个 goroutine 从通道中取值为止。同理,如果一方先收了,那么也会阻塞直到另外的 goroutine 发送,因此无缓冲通道也可以称作同步通道。

那咋办啊,假如还想射后不理,那就需要用到缓冲通道。

  1. bufferedChannel := make(chan int, 3)

满了之后,当然就是继续阻塞啦。

巧妙的,我利用了通道的阻塞的这个特性,为 WebP Server 增加 Prefetch 使用 CPU 核心数的限制,注意看 23、24 和 42 行,也不知道这算不算是正经的用法

Python程序员的Golang学习笔记

接口

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

反正我是没怎么看懂……

  1. package main
  2.  
  3. import "fmt"
  4.  
  5. // 实现一个鸭子
  6. type Duck interface {
  7. Swim() // 游泳
  8. Feathers() // 羽毛
  9. }
  10.  
  11. // 实现一个会叫的鸭子,因为嵌入了Duck,所以也有swim和feathers方法
  12. type QuackDuck interface {
  13. Quack() // 嘎嘎叫
  14. Duck // 嵌入接口
  15. }
  16.  
  17. //真实鸭子的实现
  18. type RealDuck struct{}
  19.  
  20. func (RealDuck) Swim() {
  21. fmt.Println("用鸭璞向后划水")
  22. }
  23.  
  24. func (RealDuck) Feathers() {
  25. fmt.Println("遇到水也不会湿的羽毛")
  26. }
  27.  
  28. func (RealDuck) Quack() {
  29. fmt.Println("嘎~ 嘎~ 嘎~")
  30. }
  31.  
  32. // 玩具鸭的实现
  33. type ToyDuck struct{}
  34.  
  35. func (ToyDuck) Swim() {
  36. fmt.Println("以固定的速度向前移动")
  37. }
  38.  
  39. func (ToyDuck) Feathers() {
  40. fmt.Println("白色的固定的塑料羽毛")
  41. }
  42.  
  43. func main() {
  44. var duck1 Duck
  45. duck1 = RealDuck{}
  46. duck1.Swim()
  47. duck1.Feathers()
  48.  
  49. //duck1.Quack() //报错,Duck没有Quack方法
  50.  
  51. var duck2 Duck
  52. duck2 = ToyDuck{}
  53. duck2.Swim()
  54. duck2.Feathers()
  55. //duck2.Quack() //报错,Duck没有Quack方法
  56.  
  57. var duck3 QuackDuck
  58. duck3 = RealDuck{}
  59. duck3.Swim()
  60. duck3.Feathers()
  61. duck3.Quack()
  62.  
  63. var duck4 QuackDuck
  64. duck4 = ToyDuck{} //类型错误
  65. duck4.Swim()
  66. duck4.Feathers()
  67. duck4.Quack()
  68. }

https://blog.biezhi.me/2019/01/learn-golang-interfaces.html

OOP

Golang 其实并不是那么严格的面向对象的语言,不过 struct 中嵌入其他 struct,就很接近 class 这个概念了。准确的来说,这种特性是组合,而不是继承。

定义方法时,格式是这样的 func (结构体名) 方法名 {},比如func (c Cat) sleep()

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "strconv"
  6. )
  7.  
  8. // 动物类
  9. type Animal struct {
  10. name string
  11. subject string
  12. }
  13.  
  14. // 动物的公共方法
  15. func (a *Animal) eat(food string) {
  16. fmt.Println(a.name + "喜欢吃:" + food + ",它属于:" + a.subject)
  17. }
  18.  
  19. // 猫类,继承动物类
  20. type Cat struct {
  21. // 继承动物的属性和方法
  22. Animal
  23. // 猫自己的属性
  24. age int
  25. }
  26.  
  27. // 猫类独有的方法
  28. func (c Cat) sleep() {
  29. fmt.Println(c.name + " 今年" + strconv.Itoa(c.age) + "岁了,特别喜欢睡觉")
  30. }
  31.  
  32. func main() {
  33. // 创建一个动物类
  34. animal := Animal{name: "动物", subject: "动物科"}
  35. animal.eat("肉")
  36.  
  37. // 创建一个猫类
  38. cat := Cat{Animal: Animal{name: "咪咪", subject: "猫科"}, age: 1}
  39. cat.eat("鱼")
  40. cat.sleep()
  41. }

反正我也没咋弄明白…… 参考阅读

https://learnku.com/articles/32295

文件操作

Python 中,对文件进行操作,使用 open 打开,配合不同的操作模式,然后使用 read、write 方法进行读写。

Golang 中,对文件操作最简单的可以使用 io/ioutil

  1. //读:
  2. data, _ := ioutil.ReadFile("test.txt")
  3. //写:
  4. content := []byte("hello word 你好真好")
  5. err := ioutil.WriteFile("test.txt", content, 0755)
  6. if err != nil {
  7. fmt.Println(err)
  8. }

文件不存在,会创建;存在,覆盖。

如果我们想追加的话,可以这样

  1. f, _ := os.OpenFile("test.txt", os.O_APPEND|os.O_WRONLY, 0777)
  2. f.WriteString("hello1234")
  3. f.Close()

第二个参数指定文件模式。想要一行一行的读,那么用 bufio 里的 Read 系列(ReadBytesReadStringReadLine)就可以了,不过要特别注意换行符的问题

  1. f, _ := os.Open("test.txt")
  2. b:=bufio.NewReader(f)
  3. for {
  4. a, _, c := b.ReadLine()
  5. if c == io.EOF {
  6. break
  7. }
  8. fmt.Println(string(a))
  9. }

当然了, 有一个非常严重的问题:golang 假定所读的文件都是 UTF-8 编码的,所以在 windows 下如果这么读一个记事本创建的文本文档,那自然就乱码了。

需要这几个库

golang.org/x/text/transform

golang.org/x/text/encoding/simplifiedchines

可以用transform.NewReader进行操作

  1. f, _ := os.Open("test.txt")
  2. r := transform.NewReader(f, simplifiedchinese.GBK.NewDecoder())
  3. d, _ := ioutil.ReadAll(r)
  4. fmt.Println(string(d))

也可以这样先读成 bytes,然后再转

  1. b, _ := ioutil.ReadFile("test.txt")
  2. reader := transform.NewReader(bytes.NewReader(b), simplifiedchinese.GBK.NewDecoder())
  3. d, _ := ioutil.ReadAll(reader)
  4. 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 库,基本用法总结下

  1. //显示当前时间戳
  2. time.Now().Unix()
  3.  
  4. //时间戳转换成time结构
  5. time.Unix(10,0)
  6.  
  7. //字符串转日期,需要注意Parse在缺少时区信息时,会默认0时区,所以会差8小时
  8. time.Parse("2006-01-02 15:04:05", "2020-02-01 14:08:00")
  9.  
  10. //要想不差8小时,那咱就
  11. time.ParseInLocation("2006-01-02 15:04:05", "2020-02-01 14:08:00",time.Local)

这就没问题了。2006-01-02 15:04:05是固定格式,类似传统的 ymdhms,记不住啊记不住啊!

日期转换为字符串

  1. t.Format("今天是2006年")

附带功能

time.Now()附带了AfterBeforeSubAdd等方法,方便时间运算

执行系统命令

Python 里这个方法很多,os.system()subprocess等等。Golang 有一个exec

  1. out, _ := exec.Command("ls").Output()
  2. fmt.Println(string(out))

Web 框架

在用 Python 的时候,我一般会选择 tornado 作为 web 框架,主要原因就是 tornado 的性能比较好,支持异步非阻塞,并且可以很方便的通过 self 来进行各种操作。Golang 的 web 框架也很多,比如说 beego,echo,gin。

当然了,golang 自带的net/http模块本身本身提供了很多功能,我们可以应用它创建一个 web 服务器,如下代码即可

  1. func IndexHandler(w http.ResponseWriter, r *http.Request) {
  2. fmt.Fprintln(w, "hello world")
  3. }
  4.  
  5. func main() {
  6. http.HandleFunc("/", IndexHandler)
  7. err := http.ListenAndServe("127.0.0.1:8000", nil)
  8. fmt.Println(err)
  9. }

经过一些纠结,我选择了 gin……

基础用法

  1. package main
  2.  
  3. import "github.com/gin-gonic/gin"
  4.  
  5. func main() {
  6. r := gin.Default()
  7. r.GET("/ping", func(c *gin.Context) {
  8. c.JSON(200, gin.H{
  9. "message": "pong",
  10. })
  11. })
  12.  
  13. r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
  14. }

这里 r.GET 就是路由,后面这里用了匿名函数,当然不想用匿名函数也成。

如果要支持其他 http 方法,那就这样

  1. r.POST("/ping",ping)

返回数据

c.String, c.JSON等

获取 url 参数

  • Get 参数
  1. c.Query(key)
  • post 参数
  1. c.PostForm("name")
  • 上传文件
  1. file, _ := c.FormFile("file")
  2. err := c.SaveUploadedFile(file, file.Filename)
  • 获取 raw body
  1. c.GetRawData()
  • 静态文件
  1. router.Static("/assets", "./assets")
  2. router.StaticFS("/more_static", http.Dir("my_file_system"))
  3. router.StaticFile("/favicon.ico", "./resources/favicon.ico")
  • http 重定向
  1. c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
  • 路由重定向
  1. c.Request.URL.Path = "/test2"
  2. r.HandleContext(c)
  • cookie
  1. cookie, err := c.Cookie("gin_cookie")
  2. c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", http.SameSiteLaxMode, false, true)

Requests

Python 中,我们一般使用 requests 获取 http 资源,当然用 urllib 也可以,只不过稍微有些繁琐

同理,golang 的 net/http 库已经提供了最基础的操作。

  1. resp, _ := http.Get("http://example.com/")
  2. data, _ := ioutil.ReadAll(resp.Body)
  3. fmt.Println(string(data))
  4. resp.Body.Close()

啊对了,如果返回的是 json 的话,resp.Body可以直接被Unmarshal的哦

还有一个人封装的 grequests,用起来也不错

  1. //发get
  2. resp, err := grequests.Get("http://httpbin.org/get", nil)
  3. if err != nil {
  4. log.Fatalln("Unable to make request: ", err)
  5. }
  6.  
  7. fmt.Println(resp.String())
  8.  
  9. //发post
  10. d := grequests.RequestOptions{
  11. Data: map[string]string{"hello": "world"},
  12. }
  13. resp, err := grequests.Post("http://httpbin.org/post", &d)
  14. if err != nil {
  15. log.Fatalln("Unable to make request: ", err)
  16. }
  17.  
  18. 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

  1. import (
  2. "database/sql"
  3. "fmt"
  4.  
  5. _ "github.com/go-sql-driver/mysql"
  6. )

建立连接,典型的连接是这样的,当然有些参数是可以省略的。

  1. db, _ := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test?charset=utf8mb4")
  2. defer db.Close()
  3.  
  4. //插入,直接这样参数化查询,返回一个result,一个error
  5. db.Exec("INSERT INTO user VALUES (?,?)", "Benny", 18)
  6. //或者下面这这样用prepare,没啥区别
  7. stmt, _ := db.Prepare("INSERT INTO user VALUES (?,?)")
  8. stmt.Exec("Amy", 19)
  9.  
  10. // 删除,可以这么玩
  11. db.Exec("DELETE FROM user WHERE name=?", "Amy")
  12. // 修改,我猜你已经猜到了吧
  13. db.Exec("UPDATE user SET age=? WHERE name=?", 19, "Benny")
  14.  
  15. // 查询,Query用于返回结果集,上面的Exec就不能在这里用了,可以想象成Python的execute之后再fetch
  16. data, _ := db.Query("select version();")
  17. for data.Next() {
  18. var v string
  19. data.Scan(&v)
  20. fmt.Println(v)
  21. }
  22.  
  23. // 再举一个例子?,查询一个表的数据,这个时候我们如果定义struct就更好了是吧
  24. type User struct {
  25. name string
  26. age int
  27. }
  28. data, _ := db.Query("SELECT * FROM user")
  29. var s []User
  30. for data.Next() {
  31. var user User
  32. data.Scan(&user.name, &user.age)
  33. s = append(s, user)
  34. }
  35. fmt.Println(s)

插入的时候想要用类似 pymysql 的executemany?不好意思,得自己实现了

如果需要事务,那么可以这样

  1. tx, _ := db.Begin()
  2. tx.Exec("INSERT INTO image VALUES (?,?)", "abc", "def")
  3.  
  4. tx.Commit()
  5. tx.Rollback()

MongoDB

MongoDB 官方维护了一个 mongo-go-driver,使用go get go.mongodb.org/mongo-driver即可安装

Golang 毕竟还是静态类型的语言,因此用 mongodb 还是有点折磨人的?

连接数据库

用起来,这样就建立了到数据库的连接

  1. ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)
  2. client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
  3. if err != nil {
  4. fmt.Println(err)
  5. }
  6.  
  7. defer client.Disconnect(ctx)

这个 URI,实际上就是 mongodb 的连接字符串,定义如下

  1. mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[database][?options]]

举例如下,如果数据库带认证,那么就要选择 2 和 3 了。

  1. mongodb://mongodb0.example.com:27017/admin
  2. mongodb://myDBReader:D1fficultP%40ssw0rd@mongodb0.example.com:27017/admin
  3. mongodb://user:password@example.com/?authSource=the_database&authMechanism=SCRAM-SHA-1"

连接成功之后,我们需要选择数据库,选择集合

  1. db:=client.Database("go")
  2. accountCol:=db.Collection("account")

bson

之后这个 col 就可以让我们操作数据库了,但是我们需要提前熟悉一下 golang 中的 bson

总共有 4 种类型,分别为 DMAE

D 表示 BSON 文档,有序 kv

M 与 D 相同,只是无序

A 数组

E 单个文档,只能有一对 kv

  1. d := bson.D{
  2. {"Name", "mark"},
  3. {"Age", 12},
  4. }
  5. fmt.Println(d)
  6.  
  7. // bson.M是一个map 无序的key-value集合,在不要求顺序的情况下可以替代bson.D
  8. m := bson.M{
  9. "name": "mark",
  10. "age": 12,
  11. }
  12. fmt.Println(m)
  13.  
  14. // bson.A 是一个数组
  15. a := bson.A{
  16. "jack", "rose", "jobs",
  17. }
  18. fmt.Println(a)
  19.  
  20. // bson.E是只能包含一个key-value的map
  21. e := bson.E{
  22. "name", "mark",
  23. }
  24. fmt.Println(e)

查询

查找一个,用FindOne就够了,第二个参数是查询条件,当然了我们要 decode 给一个定义好的变量

  1. var result Account
  2. col.FindOne(ctx, bson.D{}).Decode(&result)
  3. fmt.Println(result)
  4.  
  5. col.FindOne(ctx, bson.D{{"name", "Benny",}}).Decode(&result)

查询多个

  1. var results []Account
  2. cur, _ := col.Find(ctx, bson.D{})
  3. for cur.Next(ctx) {
  4. var result Account
  5. cur.Decode(&result)
  6. results = append(results, result)
  7. }
  8. cur.Close(ctx)
  9. fmt.Println(results)

插入

  1. var data=Account{
  2. Name: "Sally",
  3. Age: 12,
  4. Gender: "female",
  5. }
  6. col.InsertOne(ctx,data)

插入多个

  1. var data = []interface{}{
  2. Account{
  3. Name: "Benny2",
  4. Age: 10,
  5. Gender: "1",
  6. },
  7. Account{
  8. Name: "Benny3",
  9. Age: 40,
  10. Gender: "12",
  11. },
  12. }
  13. col.InsertMany(ctx, data)

修改

  1. col.UpdateOne(ctx,
  2. bson.M{"name": "Benny"},
  3. bson.M{"$set": bson.M{"name": "Benny!"}},
  4. )
  5.  
  6. col.UpdateMany(ctx,
  7. bson.M{"name": "Benny!"},
  8. bson.M{"$set": bson.M{"age": 14}},
  9. )

替换

  1. col.ReplaceOne(ctx,
  2. bson.M{"name": "Sally"},
  3. bson.D{{"name2", "123d"},
  4. {"job", "coder"},
  5. },
  6. )

删除

  1. col.DeleteOne(ctx,
  2. bson.M{"name": "Benny!"},
  3. )
  4. col.DeleteMany(ctx,
  5. bson.M{"name": "Benny!"},
  6. )

其他操作

基本上也都是一个套路了

  1. db.ListCollectionNames(ctx, bson.M{})
  2. client.ListDatabases(ctx,bson.M{})
  3. col.Drop(ctx)

https://www.mongodb.com/blog/post/mongodb-go-driver-tutorial

正则表达式

作为一名不太会写、会用正则表达式的小豆子,我一般都是无脑的 findall 的,有时也会使用 sub

Golang 中使用正则,emmm 我也不太会啊……

  1. text := "hello 9123 世界"
  2. re := regexp.MustCompile(`\w`)
  3. result := re.FindAllString(text,-1)
  4. fmt.Println(strings.Join(result,""))

Base64

  1. text := "hello"
  2. s := base64.StdEncoding.EncodeToString([]byte(text))
  3. fmt.Println(s)
  4. r, _ := base64.StdEncoding.DecodeString(s)
  5. fmt.Println(string(r))

读写 Excel

Python 中读写 Excel 一般使用 xlrd/xlwt 或者 openpyxl 库,基本操作思路是打开 - 选择 sheet - 操作单元格。Golang 中有一个 excelize,也可以进行类似的操作。注意,我一般选择 v2 的那个版本,所以 go get 的时候要小心哦

  1. go get https://github.com/360EntSecGroup-Skylar/excelize/v2

  1. f, err := excelize.OpenFile("sample.xlsx")
  2. if err != nil {
  3. fmt.Println(err)
  4. return
  5. }
  6. cell := f.GetCellValue("Sheet1", "A1")
  7.  
  8. //如果想要遍历的话,
  9. rows,_ := f.GetRows("Sheet1")
  10. for _, row := range rows {
  11. for _, colCell := range row {
  12. fmt.Print(colCell, "\t")
  13. }
  14. fmt.Println()
  15. }
  16.  

创建 Excel

  1. f:=excelize.NewFile()
  2. f.SetCellValue("sheet1","A1","hello123")
  3. f.SaveAs("hello.xlsx")

编辑 Excel

  1. f, _ := excelize.OpenFile("sample.xlsx")
  2. f.SetCellValue("sheet1", "A4", "edited!")
  3. f.Save()

其他辅助工具

使用过 xlrd 和 openpyxl 的盆友们可能知道,他们一个用 A1A2 这样的坐标,另外一个用 0,0,1,1 这种索引,别怕盆友们

  1. x, y, _ := excelize.CellNameToCoordinates("A1")
  2. name, _ := excelize.CoordinatesToCellName(3, 4)

想要转一列 也可以

  1. excelize.ColumnNameToNumber("B")
  2. excelize.ColumnNumberToName(5)

啥?完事了

啊对不住各位亲朋好友们,到这里就没了,那个基础语法靠自己了…… 标准库和常用库也靠自己了,或者等我慢慢更新吧

这篇笔记总结起来大概花了 500 分钟,希望能够帮助到有类似需求、有着类似的学习曲线的人。喜欢的嘛麻烦给我的项目 WebP Server Go 点个 star~谢谢亲们?❤️

Python程序员的Golang学习笔记


文章版权归原作者所有丨本站默认采用 CC-BY-NC-SA 4.0 协议进行授权 |
转载必须包含本声明,并以超链接形式注明原作者和本文原始地址:
https://dmesg.app/from-pyhon-to-go.html
喜欢 (16)
分享:-)
关于作者:
If you have any further questions, feel free to contact me in English or Chinese.
发表我的评论
取消评论

                     

去你妹的实名制!

  • 昵称 (必填)
  • 邮箱 (必填,不要邮件提醒可以随便写)
  • 网址 (选填)
(2) 个小伙伴在吐槽
  1. 特地前来点赞?!
  2. 记笔记:老熟人在思科实习过。
    编程随想 2020-04-01 22:47 回复
您直接访问了本站! 莫非您记住了我的域名. 厉害~ 我倍感荣幸啊 嘿嘿