原教程地址:微软Go入门
安装
此处不赘述,官网走起(可能需要kx上网)。
语法:
变量
声明变量:
var(变量名 类型 [= 数值]) []为可选项
var 变量名 类型
var 变量名 = xxxx
变量名 := xxx
注意,每个变量声明之后,必须有地方使用到,不然就无法run。
Go 是一种强类型语言。 这意味着你声明的每个变量都绑定到特定的数据类型,并且只接受与此类型匹配的值。(VsCode搭配官方的工具,每次ctrl+s之后,代码都会被格式化。同时,可以看到提示的各种语法问题,编译问题等等。)
Go 有四类数据类型:
基本类型:数字(int32 int64 int...)、字符串(string)和布尔值(bool)
聚合类型:数组和结构(struct)
引用类型:指针(& *)、切片(slice)、映射、函数(func)和通道(channel)
接口类型:接口(interface)
rune只是为了区分int和char,源码如下:
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32
有时,你需要对字符进行转义。 为此,在 Go 中,请在字符之前使用反斜杠 ()。 例如,下面是使用转义字符的最常见示例:
\n:新行
\r:回车符
\t:选项卡
':单引号
":双引号
\:反斜杠
iota用于自增的常量使用,效果更佳,示例如下:
type ByteSize float64
const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
挑战:可以尝试用iota赋值一周的七天。
函数(func):
语法:
func name(parameters) (results) {
body-content
}
os.Args 变量包含传递给程序的每个命令行参数。
- Go 是“按值传递”编程语言。 这意味着每次向函数传递值时,Go 都会使用该值并创建本地副本(内存中的新变量)。
在 Go 中,有两个运算符可用于处理指针:
- & 运算符使用其后对象的地址。
- 运算符取消引用指针。 也就是说,你可以前往指针中包含的地址访问其中的对象。
模块 & 库...
语法:
- 初始化模块:
go mod init 模块名
; - 升级模块:
go get 外部模块名@版本号
(其中,“@版本号”可不填,默认为@latest,即为最新版本) ; - 列出当前模块的所有依赖:
go list -m all
- 列出模块可用版本:
go list -m -versions 外部模块名
- 多个版本存在,或者有的库已经用不上了:
go mod tidy
可以去除相应的require行
举个栗子:
go mod init example.com/hello, 会获得一个go.mod文件:
module example.com/hello
go 1.15
当import 库之后,go.mod会增加require选项,比如:
import (
"rsc.io/quote"
quoteV1 "rsc.io/quote/v3"
)
查看go.mod:
module example.com/hello
go 1.15
require (
golang.org/x/text v0.3.5 // indirect
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1 // indirect
)
同时,会出现go.sum,这部分是为了确保项目所依赖的模块不会由于恶意,意外或其他原因而意外被更改。(个人理解为对文件做哈希值校验。)
关于modules的使用,参考官方博客
关于语义导入版本控制(Semantic Import Versioning),参考此处
defer/panic/recover
defer(可以推迟函数运行,类似于堆栈的后进先出。通常情况下,当你想要避免忘记任务(例如关闭文件或运行清理进程)时,可以推迟某个函数的运行。)
package main
import (
"io"
"os"
)
func main() {
f, err := os.Create("notes.txt")
if err != nil {
return
}
defer f.Close()
if _, err = io.WriteString(f, "Learning Go!"); err != nil {
return
}
f.Sync()
}
panic(内置 panic() 函数会停止正常的控制流。 所有推迟的函数调用都会正常运行。 进程会在堆栈中继续,直到所有函数都返回。 然后,程序会崩溃并记录日志消息。 此消息包含错误和堆栈跟踪,有助于诊断问题的根本原因。)
package main
import "fmt"
func main() {
g(0)
fmt.Println("Program finished successfully!")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic("Panic in g() (major)")
}
defer fmt.Println("Defer in g()", i)
fmt.Println("Printing in g()", i)
g(i + 1)
}
recover(Go 提供内置函数 recover(),允许你在出现紧急状况之后重新获得控制权。 只能在已推迟的函数中使用此函数。 如果调用 recover() 函数,则在正常运行的情况下,它会返回 nil,没有任何其他作用。)
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in main", r)
}
}()
g(0)
fmt.Println("Program finished successfully!")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic("Panic in g() (major)")
}
defer fmt.Println("Defer in g()", i)
fmt.Println("Printing in g()", i)
g(i + 1)
}
数组、切片、映射、结构
数组(定长)的例子:
q := [...]int{1, 2, 3} // 一维省略号可以帮助计算出数组的长度。
var twoD [3][5]int // 二维数组
var threeD [3][5][2]int // 二维数组
for i := 0; i < 3; i++ {
for j := 0; j < 5; j++ {
for k := 0; k < 2; k++ {
threeD[i][j][k] = (i + 1) * (j + 1) * (k + 1)
}
}
}
fmt.Println("\nAll at once:", threeD)
切片只是名为基础数组的数组之上的一种数据结构。(数据底层还是一个数组。)通过切片,可访问整个基础数组,也可仅访问部分元素。切片只有 3 个组件:
- 指针,指向基础数组可访问的第一个元素(并非一定是数组的第一个元素)。
- 长度(len),指示切片中的元素数目。(实际可见的长度)
- 容量(cap),显示切片开头与基础数组结束之间的元素数目。(实际可用长度)
例子:
package main
import "fmt"
func main() {
months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
quarter1 := months[0:3]
quarter2 := months[3:6]
quarter3 := months[6:9]
quarter4 := months[9:12]
fmt.Println(quarter1, len(quarter1), cap(quarter1))
fmt.Println(quarter2, len(quarter2), cap(quarter2))
fmt.Println(quarter3, len(quarter3), cap(quarter3))
fmt.Println(quarter4, len(quarter4), cap(quarter4))
}
Go 具有内置函数copy(dst, src []Type)
用于创建切片的副本。make([]Type, 3)
用来创建切片。(个人理解是:用这种方式来填补深拷贝和浅拷贝的差异。)
package main
import "fmt"
func main() {
letters := []string{"A", "B", "C", "D", "E"}
fmt.Println("Before", letters)
slice1 := letters[0:2]
slice2 := letters[1:4]
slice3 := make([]string, 3)
copy(slice3, letters[1:4])
slice1[1] = "Z"
fmt.Println("After", letters)
fmt.Println("Slice2", slice2)
fmt.Println("Slice3", slice3)
}
映射(map):映射是动态的。 创建项后,可添加、访问或删除这些项。(无他,就是键值对。)
// 方式1(直接给数值)
studentsAge := map[string]int{
"john": 32,
"bob": 31,
}
// 方式2(make初始化)
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31
// 访问映射中没有的项时 Go 不会返回错误,这是正常的。 但有时需要知道某个项是否存在。
// 在 Go 中,映射的下标表示法可生成两个值。 第一个是项的值。 第二个是指示键是否存在的布尔型标志。
val, exist := studentsAge["christy"]
fmt.Println("Christy's age is", studentsAge["christy"])
// 若要从映射中删除项,请使用内置函数 delete()。
delete(studentsAge, "john")
// 可使用基于范围的循环
for name, age := range studentsAge {
fmt.Printf("%s\t%d\n", name, age)
}
// 如果对string进行循环时,每个都是rune类型,但是如果取下标的话,则是byte
结构,语法及例子如下:
type 结构名 struct {
字段 类型
字段 类型
}
// (go是传值的语言,要修改还是得用指针类型。)
package main
import "fmt"
type Employee struct {
ID int
FirstName string
LastName string
Address string
}
func main() {
employee := Employee{LastName: "Doe", FirstName: "John"}
fmt.Println(employee)
employeeCopy := &employee // 不用指针改不了值
employeeCopy.FirstName = "David"
fmt.Println(employee)
}
// 结构嵌套
package main
import "fmt"
type Person struct {
ID int
FirstName string
LastName string
Address string
}
type Employee struct {
Person
ManagerID int
}
type Contractor struct {
Person // 可以不给字段名,但是初始化的时候要给清楚该字段的值
CompanyID int
}
func main() {
employee := Employee{ // 给清楚Person类型对应的属性
Person: Person{
FirstName: "John",
},
}
employee.LastName = "Doe"
fmt.Println(employee.FirstName)
}
处理Json:json.Marshal以及json.Unmarshal
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
ID int
FirstName string `json:"name"`
LastName string
Address string `json:"address,omitempty"` // omitempty忽略空值,减少传输开销。
}
type Employee struct {
Person
ManagerID int
}
type Contractor struct {
Person
CompanyID int
}
func main() {
employees := []Employee{
Employee{
Person: Person{
LastName: "Doe", FirstName: "John",
},
},
Employee{
Person: Person{
LastName: "Campbell", FirstName: "David",
},
},
}
data, _ := json.Marshal(employees) // 由于存在omitempty选项,json里没有address的内容,减少传输开销。
fmt.Printf("%s\n", data)
var decoded []Employee
json.Unmarshal(data, &decoded)
fmt.Printf("%v", decoded)
}
错误&&日志
通用做法如下,把可能的错误返回出来:
employee, err := getInformation(1000)
if err != nil {
// Something is wrong. Do something.
}
- 始终检查是否存在错误,即使预期不存在。 然后正确处理它们,以免向最终用户公开不必要的信息。(比如以前写后台的时候,数据库错误之类的,在测试的时候,被打印出来,影响到用户体验。)
- 在错误消息中包含一个前缀,以便了解错误的来源。 例如,可以包含包和函数的名称。在记录错误时记录尽可能多的详细信息,并打印出最终用户能够理解的错误。(提供更多debug的(给人看的)信息。)
- 创建尽可能多的可重用错误变量。(重用更多的错误类型,比如404之类的错误。)
- 了解使用返回错误和 panic 之间的差异。 不能执行其他操作时再使用 panic。 例如,如果某个依赖项未准备就绪,则程序运行无意义(除非你想要运行默认行为)。(尽量程序处理好各种错误,不要panic再来recover。)
记录日志(log库)、存入文件(os库):
package main
import (
"log"
"os"
)
func main() {
file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
log.SetOutput(file)
log.Print("Hey, I'm a log!")
}
Go 的几个记录框架有 Logrus、zerolog、zap 和 Apex。
以zerolog为例,安装:
go get -u github.com/rs/zerolog/log
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Print("Hey! I'm a log message!")
}
主要是为了管理日志级别比较方便,打印如下:{"level":"debug","time":1609855453,"message":"Hey! I'm a log message!"}
接口
以下创建图形接口,然后实现正方形的结构体,实现接口中的方法,就是等于实现了接口。那么,可以用图形结构体初始化一个正方形(因为已经实现了该接口,调用的是被实现的方法。)
type Shape interface {
Perimeter() float64
Area() float64
}
type Square struct {
size float64
}
func (s Square) Area() float64 {
return s.size * s.size
}
func (s Square) Perimeter() float64 {
return s.size * 4
}
func main() {
var s Shape = Square{3}
fmt.Printf("%T\n", s)
fmt.Println("Area: ", s.Area())
fmt.Println("Perimeter:", s.Perimeter())
}
实现net/http 程序包中的 http.Handler 接口:
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
// 就可以把实现的具体结构作为参数传递给ListenAndServe()的第二个参数了。
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func main() {
db := database{"Go T-Shirt": 25, "Go Jacket": 55}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
并发
需要注意:个人理解,主程序为主进程,如果在子线程执行函数的过程中,主进程的程序已经运行完了。此时,程序会直接退出,而不会等待子线程完成指令。
func main(){
login()
go func() {
launch()
}()
}
关于Channel,官方博客值得一看:Share Memory By Communicating。意思是通过通信来共享内存。
无缓冲channel(个人理解为同步的意思,有线程放进变量,必得有线程取出变量,不然就会出现Dead Lock的报错提醒。)
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
start := time.Now()
apis := []string{
"https://management.azure.com",
"https://dev.azure.com",
"https://api.github.com",
"https://outlook.office.com/",
"https://api.somewhereintheinternet.com/",
"https://graph.microsoft.com",
}
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
for i := 0; i < len(apis); i++ {
fmt.Print(<-ch)
}
elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}
func checkAPI(api string, ch chan string) {
_, err := http.Get(api)
if err != nil {
ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
return
}
ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}
有缓冲channel
func send(ch chan string, message string) {
ch <- message
}
func main() {
size := 2
ch := make(chan string, size)
send(ch, "one")
send(ch, "two")
go send(ch, "three")
go send(ch, "four")
fmt.Println("All data sent to the channel ...")
for i := 0; i < 4; i++ {
fmt.Println(<-ch)
}
fmt.Println("Done!")
}
select关键字是switch的channel版本。
如何在使用 select 关键字的同时与多个 channel 交互的简短主题。 有时,在使用多个 channel 时,需要等待事件发生。
package main
import (
"fmt"
"time"
)
func process(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "Done processing!"
}
func replicate(ch chan string) {
time.Sleep(1 * time.Second)
ch <- "Done replicating!"
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go process(ch1)
go replicate(ch2)
for i := 0; i < 2; i++ {
select {
case process := <-ch1:
fmt.Println(process)
case replicate := <-ch2:
fmt.Println(replicate)
}
}
}
测试
- 如何在 Go 中进行测试。(通过使用测试驱动开发)。
编写代码时要遵循的一个良好做法是使用测试驱动开发 (TDD) 方法。 使用此方法时,我们将首先编写测试。 我们可以肯定那些测试会失败,因为它们测试的代码还不存在。 然后,我们将编写满足测试条件的代码。
创建测试文件时,该文件的名称必须以 _test.go 结尾。 要编写的每个测试都必须是以 Test 开头的函数。 然后,你通常为你编写的测试编写一个描述性名称,例如 TestDeposit。
举例:
package bank
import "testing"
func TestAccount(t *testing.T) {
}
输出如下:
=== RUN TestAccount
--- PASS: TestAccount (0.00s)
PASS
ok github.com/msft/bank 0.391s
测试命令:
go test -v
(To be continue) 后续熟练Go,上手CRUD,尝试做个Web项目。
推荐:
effective go:如何在Go领域下,写出自己的高效。
go web wiki:写基础Web应用。