21天速成go-第四天

107 阅读10分钟

开始找实际项目

项目一步步的学着敲着来

选取原则: 最近更新 + 代码量少 + 能运行有截图有示例

第一步是 gitee.com/hhxsv5/go-r… go 的Redis 大键分析然后写入csv 这个应该很简单,作为第一下的完整项目

第二步是这个 gitee.com/shirdonl/ca… go的验证码页面服务,这个只有一个页面,入门web服务简单点

gitee.com/gzydong/Lum… go 的网页版在线聊天,这个非常完善,值得学习,最好是能打包成mac 软件

image.png

gitee.com/b0cloud/b0p… go 的多端网盘服务,这个估计比较麻烦,最后再看

gitee.com/mirrors/Bad… 一个可嵌入,持久,简单,快速的键值(KV)存储,纯 go 编写

gitee.com/wanyuxiang0… mgface.com-使用go语言来实现分布式对象存储

gitee.com/dubbogo/dub… Apache Dubbo go语言实现

gitee.com/jqncc/OAM 也是web的增删改查 GO-OAM是基于go语言开发的web版运维资源管理系统,算是简版的CMDB,将各种难记的账号、密码、主机、文档、应用等资源管理起来,以项目方式整合

gitee.com/nway/websql 也是web的增删改查 web版数据库管理工具,由go语言编写,可以做到无依赖跨平台,编译后无需安装运行环境,支持本地模式和远程部署模式,支持基于操作系统的人脸/指纹识别登录。开源协议宽松,可以自由使用,以满足个人和企业需求。

gitee.com/mirrors/git… 这个不知道是什么,可能不需要太关注 Gitea的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用Go作为后端语言,这使我们只要生成一个可执行程序即可。

另外剩下的就是

github.com/ouqiang/goc… 最经典部分,而且是亲身使用过 非常好用的一个定时任务工具

github.com/ouqiang/sha… ssr 的go

github.com/ouqiang/sup… Supervisor是*nix环境下的进程管理工具, 可以把前台进程转换为守护进程, 当进程异常退出时自动重启. supervisor-event-listener监听进程异常退出事件, 并发送通知.

或者直接在 github.com/topics/go 这里查看最热的代码仓库

阅读GolangTraining的笔记

字符串格式化中 %x %X 的区别就是一个是小写字母,一个是大写字母来表示16进制,比如 1a3f和 1A3F的区别

%q 就是代表单个字符的字面量, %q 也能当做%s的输出,就是会附带上自动转义,比如

str := "This is a "string" with $igns and \chars"
fmt.Printf("The quoted string: %q\n", str)
fmt.Printf("The quoted string: %s\n", str)

这样是输出

The quoted string: "This is a \"string\" with $igns and \\chars"
The quoted string: This is a "string" with $igns and \chars

%s 是输出字符并且不做任何转义,不管你的字符中包含什么内容,都将会原样输出,类似于 反引号的效果

还有一种是 %v 的输出

image.png

%v 是用默认的格式进行输出,根据你的变量是什么,就输出什么样的格式

  • **
    布尔型**:输出为 true 或 false

  • 整数型:以十进制形式输出。

  • 浮点数:以浮点数形式输出,精度由编译器决定。

  • 复数:输出为 (real ± imagi),其中 real 是实部,imag 是虚部。

  • 字符串:直接输出字符串内容,不转义特殊字符。

  • 通道:输出通道的描述,通常是类型信息。

  • 接口:输出接口内部存储的值(不是接口本身),类似于 %v

  • 指针:输出指针指向的值,而不是指针的内存地址。

  • 结构体:输出结构体的字段,格式为 {field1:value1 field2:value2 ...}

  • 切片:输出切片中的所有元素,格式为 [value1 value2 ...]

我感觉最重要的就是 数组和字典的输出,能直观的看得到

h := []int{1, 2, 3, 4, 5}
i := map[int]int{
   1: 1,
   2: 2,
   3: 3,
}

就是输出


h := []int{1, 2, 3, 4, 5}
i := map[int]int{
   1: 1,
   2: 2,
   3: 3,
}

如果需要更精确的控制格式,可以使用其他的占位符,%v 经常用于那些你不知道变量类型或者不关心变量类型是什么的时候,他提供了一种方便的格式来获取变量的字符串表达的形式

另外还有 %T 的占位符,就是类似于type的意思,用于输出变量的类型

image.png

image.png

同样的 %v 还给了一个样例,

fmt.Printf("nilPtr is nil? %v\n", nilPtr == nil) // 输出: nilPtr is nil? true

这样 true false 也能输出,不过本来这就是bool类型,%v 感觉就像是verbose 单词一样

03

变量的定义,如果是一个类型可以写到一行 var a,b,c int 或者同时赋值 var a, b ,c int = 1,2,3

对于字符串来说,居然还是可以

var message = "ok"

直接这样推断赋值,而不需要 var message string message = "ok'

同样的推断,还能用于int上,比如刚才的a b c 同时赋值1,2,3也可以写成

var a,b,c = 1,2,3

不需要后面声明类型了

或者可以同时推断不同的类型

var a, b, c, d, e, f = 1, false, 3, "ok", []int{1, 2, 3}, map[int]int{1: 2, 3: 4}

这样子的和 := 相比就能一次赋值多个了,不需要 := 很多行

刚才说错了,直接的 := 也可以复制多次

a, b, c := 1, false, 3

如果使用了var 那就不需要使用: 使用了: 就不需要使用 var

04

如果在同一个包里面的变量,那么可以在一个文件里面定义变量,然后另外一个文件里面直接引用,无论函数内外都能引用,效果类似于全局变量的效果

所以感觉是可以把全局变量,单独写在一个包的一个单独的go文件里面的

image.png

比如这里,虽然在printer里面没有任何关于MyName的定义,但是那是因为在name.go单独的文件里面定义了,而他们又是属于同一个包下的,所以可以当成全局变量来使用

对于这种写法


func main() {
   x := 42
   fmt.Println(x)
   {
      fmt.Println(x)
      y := "The credit belongs with the one who is in the ring."
      fmt.Println(y)
   }
   //fmt.Println(y) // outside scope of y
}

他不是定义了一个函数,而是定义了一个代码块,固定了一个作用域,注释掉的那一行的y就取不到,因为在作用域外了,嵌套代码块并不是创建新的函数,而是提供了一种定义局部作用域的方式

代码块适用于组织代码和定义作用域的语法结果

对于这样一段代码,他的输出居然是1 和 2 x 居然被影响到了

func main() {
   x := 0
   increment := func() int {
      x++
      return x
   }
   fmt.Println(increment())
   fmt.Println(increment())
   
}

这是因为increment是一个闭包,变量x是被闭包捕获到了的变量,捕获到了,就会影响到这个变量。 在go中,当你在韩硕或者闭包中对一个变量进行修改,如果这个变量是在外部作用域声明的,且闭包捕获到了这个变量,那么对这个变量的所有修改都是对原始变量的修改

如果改成参数传递的话,就是正常的输出 1 1 了

func main() {
   //x := 0
   //increment := func() int {
   // x++
   // return x
   //}
   //fmt.Println(increment())
   //fmt.Println(increment())

   x := 0
   increment := func(x int) int {
      x++
      return x
   }
   fmt.Println(increment(x))
   fmt.Println(increment(x))

}

这里还有一个点,函数的签名是

func(x int) int

之前是看过能吧返回值写到函数定义里面的,于是想写成

func(x int) (n int) 结果发现不行,这是因为没有充分理解命名值返回,所谓的命名值返回,最大的好处就是能直接从函数定义里面,看到返回的内容,而不需要看里面的细节的return,认为是省略return的一种写法,以下是几个典型的例子

package main

import (
   "fmt"
)

func say(a int) (b int) {
   b = a + 1
   return
}

func main() {
   fmt.Println(say(1))
}

以上代码不是一次性写出来了,改了几次,主要改动的地方是:

  1. 如果想要跑的话,包名也必须是main 才行,不然没有run 出来,新建go文件的时候,自动创建的包名是文件夹的名字了不是package main 导致没有找到run出来
  2. 写say的时候,我想着因为指定了b会返回 return就不写了,结果发现必须强制让你写return,也就是说,你虽然做了 命名值返回,但是结尾的return还是要写,那么请问这有什么必要呢,return也没有省略掉啊

更加明显的效果大概是这样子的,比如 max()函数

func maxValue(a, b int) (ans int) {
   if a > b {
      ans = a
   } else {
      ans = b
   }
   return
}

func main() {
   fmt.Println(maxValue(1, 2))
}

但是还有一点更奇怪的是,if else 强制式Java这种写法,C风格的if else 无法使用 会报语法错误

if a > b {
   ans = a
}
else {
   ans = b
}

unexpected else, expected } 反正就是 if else 强制使用大括号且必须跟着不能换行使用

另外命名变量还有一种写法是用于错误处理,比如

func divided(a, b float64) (ans float64, err error) {
   if b == 0 {
      err = fmt.Errorf("cannot divied by zero")
      return
   }
   ans = a / b
   return
}

func main() {
   fmt.Println(divided(1, 0))
}

这里写的时候有几个要注意的,第一个就是 err 不是 := 而是直接 = 即可,另外错误的类型error 可以直接使用

另外命名变量还有一种写法是延迟执行,在返回前修改命名返回值,比如如下代码


func test() (result int) {
   defer func() {
      result++
      return
   }()
   return 5
}

func main() {
   fmt.Println(test())
}

这个输出居然是6,为什么是6呢,如果把result++ 去掉,输出为5的话,这样就可以解释了 在go中,当函数声明了命名返回值,go 会自动在函数体内创建一个同名的变量,其作用域覆盖整个函数体,这个变量在函数体内部任意地方都是可见的,并且会被初始化为其类型的零值

如果没有result++的话,那么输出就是5,因为执行 return 5 返回值的就是5,而你func 又要返回值,返回值又是result, 那么return 5就相当于 result=5 然后return result

而defer 里面如果有result++的话,那么result就捕获了,注意又是用词的捕获,捕获了result值,然后访问并修改result

命名返回值来构建结果对象

代码样例如下

type User struct {
   Name  string
   Email string
}

func CreateUser(name, email string) (user User, err error) {
   user = User{
      Name:  name,
      Email: email,
   }
   err = nil
   return
}

func main() {
   fmt.Println(CreateUser("aaa", "bbb@bb.com"))
   //fmt.Printf("%v", CreateUser("aaa", "bbb@bb.com"))
}

这里注意一点,user 的创建居然不是 := 和 err 一样是直接等于就可以了

实际上,创建一个类的话,确实是

user := User{ Name: "Alice", Email: "alice@example.com", }

:=的写法,但是这里为什么可以用等于号呢,是因为提前创建了变量,才能用等于呢,因为在定义函数的时候,声明了命名返回值, (user User, err error) 相当于就执行了一个变量声明的操作,所以后面就可以直接使用 = 来赋值, err 可以直接等于也是同理

这种写法的话,等于直接先把要返回的给复制了,然后直接return,就会自然而然的return好,而不需要做什么其他的操作

命名返回值来进行多级错误处理

展示代码如下

func performAction() (status string, err error) {

   defer func() {
      if r := recover(); r != nil {
         err = fmt.Errorf("recover from panic: %v", r)
      }
   }()
   panic("error in performAction")
   status = "success"
   return
}

func main() {
   fmt.Println(performAction())

   status, err := performAction()
   fmt.Printf("%v, %v", status, err)
}

输出结果是 recover from panic: error in performAction

如果没有panic 发生,那么就是设置了status="success" 如果发生了,那么status 就是空”“

命名返回值简化多个返回值的函数

func reterieveData() (data []byte, count int, err error) {
   data = make([]byte, 1024)
   count = 100

   //panic("error")
   return
}

func main() {
   fmt.Println(reterieveData())
}

如果不注释掉panic 就会返回很多个0000 然后是100 然后是nil,如果出现了panic 的话,那么就会报错

panic: error

goroutine 1 [running]:
main.reterieveData(...)
        GolangTraining/04_scope/02_block-scope/02_closure/03_06/main.go:9
main.main()
        GolangTraining/04_scope/02_block-scope/02_closure/03_06/main.go:14 +0x3b

因为你的异常没有捕获