20個Golang片段讓我不再健忘

2023-06-06 21:00:56

前言

本文使用程式碼片段的形式來解釋在 go 語言開發中經常遇到的小功能點,由於本人主要使用 java 開發,因此會與其作比較,希望對大家有所幫助。

1. hello world

新手村的第一課,毋庸置疑。

package main

import "fmt"

func main() {
	fmt.Printf("hello world")
}

2. 隱形初始化

package main

import "fmt"

func main() {
	load()
}

func load() {
	fmt.Printf("初始化..手動%s 不錯\n", "1")
}

func init() {
	fmt.Printf("隱形初始化。。\n")
}

在 go 中定義 init 函數,程式在執行時會自動執行。類似使 junit 的 [@before](https://my.oschina.net/u/3870904) 註解。

3. 多模組的存取

java 中 package 包的概念,go 是通過資料夾 + package 關鍵字來定義的。

一般而言,我們會通過go init來建立專案,生成的go.mod檔案位於根目錄。

常見的實踐是,建立資料夾並且保持 package 名稱與資料夾保持一致。這樣 import 的永遠是資料夾,遵循以上規則則意味著資料夾的名稱即為模組名。

同一個 package 可以建立多個 .go 檔案,雖然分佈在不同的檔案中。但是他們中的方法名稱不能相同。需要注意,這裡與 java中不同類中方法可以重名不同。

此外,也沒有諸如private、protected、public等包存取許可權關鍵字。只要定義的函數首字母為大寫。則可以被外部成功呼叫。

來看一下範例:

go-tour
└── ch3
    ├── model
    │   └── test
    │   │   ├── testNest.go
    │   └── helper.go
    │   └── helper2.go
    │  
    └── main.go           
    └── go.mod

此處,ch3、model、test 均為資料夾,也可以說是 packagehelper.go 位於 model 下,它的程式碼如下:

package model

import "fmt"

var AppName = "bot"
var appVersion = "1.0.0"

func Say() {
	fmt.Printf("%s", "hello")
}

func init() {
	fmt.Printf("%s,%s", AppName, appVersion)
}

再來看看 main.go

package main

import (
	"ch3/model"
	"ch3/model/test"
)

func main() {
	model.Say()
}

顯然它的呼叫是通過 packageName.MethodName() 來使用的。需要注意的是,一個 go.mod 下只能有一個 main 包。

4. 參照外部庫

和 java 的 maven 類似,go 幾經波折也提供了官方倉庫。如下,通過 go get github.com/satori/go.uuid 命令即可安裝 uuid 庫,未指定版本,因此下載的為最新版本。

使用時是這樣的:

package main

import (
	"fmt"
	uuid "github.com/satori/go.uuid"
)

func main() {

	uuid := uuid.NewV4()
	fmt.Printf("%s", uuid)
}

5. 陣列字典和迴圈

直接看程式碼就是了。

package main

import "fmt"

var item []int
var m = map[int]int{
	100: 1000,
}
var m2 = make(map[int]int)

func main() {

	for i := 0; i < 10; i++ {
		item = append(item, i)
		m[i] = i
		m2[i] = i
	}

	for i := range item {
		fmt.Printf("item vlaue=%d\n", i)
	}

	for key, value := range m {
		fmt.Printf("m:key=%d,value=%d\n", key, value)
	}

	for _, value := range m2 {
		fmt.Printf("m2:value=%d\n", value)
	}
}

  • := 的形式只能在方法內
  • 全域性的只能用 var x=..
  • map輸出沒有順序

6. 結構體和JSON

go 中通過 struct 來定義結構體,你可以把它簡單理解為物件。一般長這樣。

type App struct {
	AppName    string
	AppVersion string `json:"app_version"`
	appAuthor  string "pleuvoir"
	DefaultD   string "default"
}

我們經常在 java 程式中使用 fastjson 來輸出 JSON字串。 go 中自帶了這樣的類庫。

package main

import (
	app2 "app/app" //可以定義別名
	"encoding/json"
	"fmt"
)

func main() {

	a := app2.App{}
	fmt.Printf("%s\n", a)

	app := app2.App{AppName: "bot", AppVersion: "1.0.1"}

	json, _ := json.Marshal(app) //轉換為字串

	fmt.Printf("json is %s\n", json)
}

  • 結構體中 JSON 序列化不會轉變大小寫,可以指定它輸出的 key名稱通過 json:xxx 的描述標籤。
  • 結構體中的預設值賦值了也不展示

7. 例外處理

作為一個有經驗的程式設計師:),go 的例外處理涉及的很簡單,也往往為人所詬病。比如滿螢幕的 err 使用。

package main

import (
	"fmt"
	"os"
)

func _readFile() (int, error) {
	file, err := os.ReadFile("test.txt")
	if err != nil {
		fmt.Printf("error is = %s\n", err)
		return 0, err
	}
	fmt.Printf("file = %s \n", file)
	return len(file), err
}

func readFile() (int, error) {
	fileLength, err := _readFile()
	if err != nil {
		fmt.Printf("異常,存在錯誤 %s\n", err)
	}
	return fileLength, err
}

func main() {
	fileLength, _ := readFile()
	fmt.Printf("%d\n", fileLength)

}

和 java 不同,它支援多返回值,為我們的使用帶來了很多便利。如果不需要處理這個異常,可以使用 _ 忽略。

8. 非同步

千呼萬喚始出來,令人興奮的非同步。

package main

import (
	"bufio"
	"fmt"
	"os"
)

func worker() {
	for i := 0; i < 10; i++ {
		fmt.Printf("i=%d\n", i)
	}
}
func main() {

	go worker()
	go worker()

	//阻塞 獲取控制檯的輸出
	reader := bufio.NewReader(os.Stdin)
	read, err := reader.ReadBytes('\n') //注意是單引號 回車後結束控制檯輸出
	if err != nil {
		fmt.Printf("err is =%s\n", err)
		return
	}
	fmt.Printf("read is %s \n", read)
}

如此的優雅,如此的簡單。只需要一個關鍵字 go 便可以啟動一個協程。我們在 java 中經常使用的是執行緒池,而在 go 中也存在協程池。據我觀察,部分協程池 benchmark 的效能確實比官方語言關鍵字高很多。

9. 非同步等待

這裡就類似 java 中使用 countdownLatch 等關鍵字空值並行程式設計中程式的等待問題。

package main

import (
	"fmt"
	"sync"
	"time"
)

func upload(waitGroup *sync.WaitGroup) {
	for i := 0; i < 5; i++ {
		fmt.Printf("正在上傳 i=%d \n", i)
	}
	time.Sleep(5 * time.Second)
	waitGroup.Done()
}

func saveToDb() {
	fmt.Printf("儲存到資料庫中\n")
	time.Sleep(3 * time.Second)
}

func main() {

	begin := time.Now()
	fmt.Printf("程式開始 %s \n", begin.Format(time.RFC850))

	waitGroup := sync.WaitGroup{}
	waitGroup.Add(1)

	go upload(&waitGroup)
	go saveToDb()
	waitGroup.Wait()

	fmt.Printf("程式結束 耗時 %d ms ", time.Now().UnixMilli()-begin.UnixMilli())
}

sync 包類似於 J.U.C 包,裡面可以找到很多並行程式設計的工具類。sync.WaitGroup 便可以簡簡單單認為是 countdownLatch 吧。也不能多次呼叫變為負數,否則會報錯。

注意,這裡需要傳入指標,因為它不是一個參照型別。一定要通過指標傳值,不然程序會進入死鎖狀態。

10. 管道

package main

import (
	"fmt"
	"sync"
)

var ch = make(chan int)
var sum = 0 //是執行緒安全的

func consumer(wg *sync.WaitGroup) {
	for {
		select {
		case num, ok := <-ch:
			if !ok {
				wg.Done()
				return
			}
			sum = sum + num
		}
	}
}

func producer() {
	for i := 0; i < 10_0000; i++ {
		ch <- i
	}
	close(ch) //如果不關閉則會死鎖
}

func main() {

	wg := sync.WaitGroup{}
	wg.Add(1)
	go producer()
	go consumer(&wg)

	wg.Wait()
	fmt.Printf("sum = %d \n", sum)
}

這裡演示的是什麼呢?管道類似一個佇列,進行執行緒間資料的傳遞。當關閉時消費端也退出,如果沒關閉管道,執行時會報死鎖。可以看出全域性變數線上程間是安全的。

可以衍生出一種固定寫法:

//固定寫法
func consumer(wg *sync.WaitGroup) {
	for {
		select {
		case num, ok := <-ch:
			if !ok {
				wg.Done()
				return
			}
			sum = sum + num
		}
	}
}

11. 介面

package main

import "fmt"

type Person interface {
	Say()
	SetName(name string)
}

type ZhangSan struct {
	Value string
}

func (z *ZhangSan) Say() {
	fmt.Printf("name=%s", z.Value)
}

func (z *ZhangSan) SetName(name string) {
	z.Value = name + ":hehe"
}

func main() {
	zhangSan := ZhangSan{}
	zhangSan.SetName("pleuvoir")
	zhangSan.Say()
}

如上的程式演示了介面的使用。

  • go的介面沒有強依賴
  • 通過結構體 + 方法的形式實現,注意方法傳入的可以是參照也可以是值

12. 鎖

package main

import (
	"fmt"
	"sync"
)

type Number struct {
	Value int
	mutex sync.Mutex //加鎖
}

func (receiver *Number) Add() {
	receiver.mutex.Lock()
	defer receiver.mutex.Unlock() //退出時會執行
	receiver.Value = receiver.Value + 1
	//fmt.Printf("add\n")
}

func (receiver *Number) Get() int {
	receiver.mutex.Lock()
	defer receiver.mutex.Unlock()
	return receiver.Value
}

func main() {
	number := Number{Value: 0}

	wg := sync.WaitGroup{}

	n := 100_0000
	wg.Add(n)

	for i := 0; i < n; i++ {
		go func(wg *sync.WaitGroup) {
			number.Add()
			wg.Done()
		}(&wg)
	}

	wg.Wait()
	fmt.Printf("count=%d", number.Get())
}

這裡是什麼?顯然就像是顯示鎖的 ReentrantLock 的使用,相信大家都能看懂。這裡出現了新關鍵字 defer,我暫且是理解為 finally。不知道你怎麼看?

13. 讀寫組態檔

這也是一個很常規的功能,看看怎麼實現。

package main

import (
	"encoding/json"
	"fmt"
	"os"
)

type Preferences struct {
	Name    string  `json:"name"`
	Version float64 `json:"version"`
}

const configPath = "config.json"

func main() {

	preferences := Preferences{Name: "app", Version: 100.01}
	marshal, err := json.Marshal(preferences)

	err = os.WriteFile(configPath, marshal, 777)
	if err != nil {
		fmt.Printf("寫入組態檔錯誤,%s\n", err)
		return
	}

	//讀取組態檔
	file, err := os.ReadFile(configPath)
	if err != nil {
		fmt.Printf("讀取檔案錯誤,%s\n", err)
		return
	}
	fmt.Printf("%s\n", file) //{"name":"app","version":100.01}

	//構建一個物件用來序列化
	readConfig := Preferences{}

	//反序列化
	err = json.Unmarshal(file, &readConfig)
	if err != nil {
		fmt.Printf("組態檔轉換為JSON錯誤,%s\n", err)
	}

	fmt.Printf("%v", readConfig) //{app 100.01}

這裡挺沒意思的,寫入 JSON 字串,然後讀取回來在載入到記憶體中。不過,簡單的範例也夠說明問題了。

14. 宕機處理

這是類似於一種最上層異常捕獲的機制,在程式的入口處捕獲所有的異常。

package main

import (
	"fmt"
	"time"
)

func worker() {
	//defer func() {  //不能寫在主函數,最外層catch沒啥用
	//	if err := recover(); err != nil {
	//		fmt.Printf("%s", err)
	//	}
	//}()
	defer recovery()
	panic("嚴重錯誤")
}

func recovery() {
	if err := recover(); err != nil {
		fmt.Printf("宕機了。%s\n", err)
	}
}

func main() {
	for true {
		worker()
		time.Sleep(1 * time.Second)
	}
}

註釋寫的很清楚,聰明的你一看就懂。

15. 單元測試

與 java 不同,go 建議單元測試檔案儘可能的離原始碼檔案近一些。比如這樣:

go-tour
    └── main.go      
    └── main_test.go  

並且它的命名也是這樣簡單粗暴:

package main

import (
	"testing"
)

func TestInit(t *testing.T) {
	t.Log("heh")

	helper := PersonHelper{}
	helper.init("pleuvoir")
	t.Log(helper.Name)
}

以大寫的 Test 開頭,檔名稱以 _test 結尾,很清爽的感覺。

16. 啟動傳參

這也是一個很常用的知識點。這裡有兩種方式:

  • 直接傳
  • 使用 flag
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
)

func main() {

	//第一種方式
	args := os.Args

	for i, arg := range args {
		println(i, arg)
	}

	//第二種方式
	config := struct {
		Debug bool
		Port  int
	}{}

	flag.BoolVar(&config.Debug, "debug", true, "是否開啟debug模式")
	flag.IntVar(&config.Port, "port", 80, "埠")

	flag.Parse()

	json, _ := json.Marshal(config)

	fmt.Printf("json is %s\n", json)
}

我建議使用第二種,更便捷自帶型別轉換,還可以給預設值,非常好。

17. 優雅退出



package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
)

func quit() {
	println("執行一些清理工作。。")
}

//正常的退出
//終端 CTRL+C退出
//異常退出

func main() {

	defer quit()
	println("進來了")

	//讀取訊號,沒有一直會阻塞住
	exitChan := make(chan os.Signal)

	//監聽訊號
	signals := make(chan os.Signal)
	signal.Notify(signals, syscall.SIGINT, syscall.SIGQUIT)

	go func() {
		//有可能一次接收到多個
		for s := range signals {
			switch s {
			case syscall.SIGINT, syscall.SIGQUIT:
				println("\n監聽到作業系統訊號。。")
				quit() //如果監聽到這個訊號沒處理,那麼程式就不會退出了
				if i, ok := s.(syscall.Signal); ok {
					value := int(i)
					fmt.Printf("是訊號型別,準備退出 %d", value)
				} else {
					println("不知道是啥,0退出")
					os.Exit(0)
				}
				//	os.Exit(value)
				exitChan <- s
			}
		}
	}()

	println("\n程式在這裡被阻塞了。")
	<-exitChan
	//panic("heh")
	println("\n阻塞被終止了。")
}

這其實是在監聽作業系統的訊號,java 中也有類似的回撥的介面(我忘了名字)。

18. 反射

作為一門高階語言,反射肯定是有的。還是使用 reflect 包。

package main

import (
	"fmt"
	"reflect"
)

type Person struct {
	Name string `json:"name"`
}

func (p *Person) SetName(name string) {
	p.Name = name
}

func (p *Person) GetName() (string, string) {
	return p.Name, "1.0.1"
}

func worker1() {
	p := Person{}
	p.SetName("pleuvoir")
	name, _ := p.GetName()
	fmt.Printf(name)
}

// 獲取方法
func worker2() {
	p := Person{}
	rv := reflect.ValueOf(&p)
	value := []reflect.Value{reflect.ValueOf("peluvoir")}
	rv.MethodByName("SetName").Call(value)
	values := rv.MethodByName("GetName").Call(nil)
	for i, v := range values {
		fmt.Printf("\ni=%d,value=%s\n", i, v)
	}
}

func worker3() {
	s := Person{}
	rt := reflect.TypeOf(s)
	if field, ok := rt.FieldByName("Name"); ok {
		tag := field.Tag.Get("json")
		fmt.Printf("tag is %s \n", tag)
	}
}

func main() {
	//正常獲取
	worker1()
	//獲取方法
	worker2()
	//獲取標籤
	worker3()
}

沒什麼好說的,寫程式碼全靠猜。

19. atomic

類似 java 中的 atomic 原子變數。

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {

	workers := 1000

	wg := sync.WaitGroup{}
	wg.Add(workers)
	for i := 0; i < workers; i++ {
		go worker2(&wg)
	}
	wg.Wait()

	fmt.Printf("count = %d", count)
}

var count int64 = 0

func worker1(wg *sync.WaitGroup) {
	count++
	wg.Done()
}

func worker2(wg *sync.WaitGroup) {
	atomic.AddInt64(&count, 1) //特別簡單
	wg.Done()
}

真的是特別簡單。

20. 執行緒安全的Map

類似於ConcurrentHashMap,與普通的 api 有所不同。

var sessions = sync.Map{}
sessions.Store(uuid, uuid)
load, ok := sessions.Load(value.Token)
		if ok {
			// 做你想做的事情
		}

21. return func

這裡就是函數式變成的例子了。函數是一等公民可以作為引數隨意傳遞。java 什麼時候能支援呢?


package main

import "fmt"

func main() {
	engine := Engine{}
	engine.Function = regular()

	function := engine.Function

	for i := 0; i < 3; i++ {
		s := function("pleuvoir")
		fmt.Printf("s is %s\n", s)
	}

}

type Engine struct {
	Function func(name string) string
}

func regular() (ret func(name string) string) {
	fmt.Printf("初始化一些東西。\n")
	return func(name string) string {
		fmt.Printf("我是worker。name is %s\n", name)
		return "我是匿名函數的返回值"
	}
}

比如這裡,如果要初始化紀錄檔什麼。最後需要讓框架在哪裡列印紀錄檔,就需要將這個初始化的紀錄檔範例傳遞過去。總而言之,言而總之。會需要讓程式碼各種傳遞。

這種方式在於第一次呼叫的時候會執行上面的程式碼片段,後面只是儲存了這個函數的控制程式碼,然後可以一直呼叫這個匿名函數。

22. context

package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	worker1()
}

func worker1() {

	//總共2秒超時
	value := context.WithValue(context.Background(), "token", "pleuvoir")
	timeout, cancelFunc := context.WithTimeout(value, 5*time.Second)
	defer cancelFunc()

	//模擬任務
	fmt.Println("開始任務")
	deep := 10
	go handler(timeout, deep)

	fmt.Println("開始阻塞", time.Now())
	//等待主執行緒超時,阻塞操作
	select {
	case <-timeout.Done():
		fmt.Println("阻塞結束", timeout.Err(), time.Now())
	}

}

// 模擬任務處理,迴圈下載圖片等
func handler(timeout context.Context, deep int) {

	if deep > 0 {
		fmt.Printf("[begin]token is %s %s deep=%d\n", timeout.Value("token"), time.Now(), deep)
		time.Sleep(1 * time.Second)
		go handler(timeout, deep-1)
	}

	//下面的哪個先返回 先執行哪個
	//如果整體超時 或者 當前方法超過2秒 就結束
	select {

	//等待超時會返回
	case <-timeout.Done():
		fmt.Println("超時了。", timeout.Err())
		//等待這麼久 然後會返回 這個函數可不是比較時間,這裡其實是在模擬處理任務,固定執行一秒 和休息一秒效果一樣
		//但是休息一秒的話就不會實時返回了,所以這裡實際應用可以是一個帶超時的回撥?
	case <-time.After(time.Second):
		fmt.Printf("[ end ]執行完成耗時一秒     %s %d\n", time.Now(), deep)
	}
}

作用:在不同的協程中傳遞上下文。

  • 傳值 類似於threadLocal
  • 可以使用超時機制,無論往下傳遞了多少協程,只要最上層時間到了 後面的都不執行
  • 俄羅斯套娃一次一層包裝

23. 字串處理

這是最高頻率的操作了,使用任何語言都無法錯過。

package main

import (
	"fmt"
	"strings"
)

func main() {

	str := " pleuvoir  "

	trimSpace := strings.TrimSpace(str)

	fmt.Printf("去除空格 %s\n", trimSpace)

	subString := trimSpace[4:len(trimSpace)]
	fmt.Printf("subString after is %s\n", subString)

	prefix := strings.HasPrefix(subString, "vo")
	fmt.Printf("是否有字首 vo : %v\n", prefix)

	suffix := strings.HasSuffix(subString, "ir")
	fmt.Printf("是否有字尾 ir : %v\n", suffix)

	builder := strings.Builder{}
	builder.WriteString("hello")
	builder.WriteString(" ")
	builder.WriteString("world")

	fmt.Printf("stringBuilder append is %s\n", builder.String())

	eles := []string{"1", "2"}

	join := strings.Join(eles, "@")
	fmt.Printf("join after is %s\n", join)

	//拼接格式化字串,並且能返回
	sprintf := fmt.Sprintf("%s@%s", "1", "20")
	fmt.Printf("Sprintf after is %s\n", sprintf)

	//列印一個物件 比較清晰的方式
	person := struct {
		Name string
		Age  int
	}{"pleuvoir", 18}
	fmt.Printf("%v", person) // 輸出 {Name:pleuvoir Age:18}
}

主要是使用 fmt 包。

24. 任務投遞

如果說使用 go 最激動人心的是什麼?是大量的協程。如果在下載任務中,我們可以啟動很多協程進行分片下載。如下,即展示使用多路複用高速下載。

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {

	chunks := 10 //檔案分成n份
	workers := 5 //個執行緒處理

	wg := sync.WaitGroup{}
	wg.Add(chunks)

	jobs := make(chan int, chunks) //帶緩衝的管道 等於任務數

	for i := 0; i < workers; i++ {
		go handler1(i, jobs, &wg)
	}

	//將任務全部投遞給worker
	scheduler(jobs, chunks)

	wg.Wait()

	fmt.Println("download finished .")
}

// 分成 chunks 份任務 裡分發
// 將 n 份下載任務都到管道中去,這裡管道數量等於 任務數量n 管道不會阻塞
func scheduler(jobs chan int, chunks int) {
	for i := 0; i < chunks; i++ {
		//time.Sleep(time.Duration(rand.Intn(3)) * time.Second)
		jobs <- i
	}
}

// 寫法2
// 注意這裡的是直接接受管道,這也是一種固定寫法,下面的 range jobs 可以認為是阻塞去搶這個任務,多個執行緒都在搶任務
func handler2(workerId int, jobs <-chan int, wg *sync.WaitGroup) {
	for job := range jobs {
		//	fmt.Printf("workerId[%d] job[%d] start download .\n", workerId, job)
		time.Sleep(1 * time.Second)
		fmt.Printf("workerId[%d] job[%d] download ok.\n", workerId, job)
		wg.Done() //這裡不要break,這樣執行完當前的執行緒就能繼續搶了
	}
}

// 寫法1,select case 多路複用
func handler1(workerId int, jobs chan int, wg *sync.WaitGroup) {
	for {
		select {
		case job, _ := <-jobs:
			//	fmt.Printf("workerId[%d] job[%d] start download .\n", workerId, job)
			time.Sleep(3 * time.Second)
			fmt.Printf("workerId[%d] job[%d] download ok.\n", workerId, job)
			wg.Done() //這裡不要break,這樣執行完當前的執行緒就能繼續搶了
		}
	}
}

後語

以上都是一個新手 Gopher 的經驗總結,文中難免有錯誤,懇請指正。

作者:京東零售 付偉

來源:京東與開發者社群