本文使用程式碼片段的形式來解釋在 go
語言開發中經常遇到的小功能點,由於本人主要使用 java
開發,因此會與其作比較,希望對大家有所幫助。
新手村的第一課,毋庸置疑。
package main
import "fmt"
func main() {
fmt.Printf("hello world")
}
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)
註解。
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
均為資料夾,也可以說是 package
。helper.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
包。
和 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)
}
直接看程式碼就是了。
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)
}
}
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 的描述標籤。作為一個有經驗的程式設計師:),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
不同,它支援多返回值,為我們的使用帶來了很多便利。如果不需要處理這個異常,可以使用 _
忽略。
千呼萬喚始出來,令人興奮的非同步。
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
的效能確實比官方語言關鍵字高很多。
這裡就類似 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
吧。也不能多次呼叫變為負數,否則會報錯。
注意,這裡需要傳入指標,因為它不是一個參照型別。一定要通過指標傳值,不然程序會進入死鎖狀態。
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
}
}
}
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()
}
如上的程式演示了介面的使用。
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
。不知道你怎麼看?
這也是一個很常規的功能,看看怎麼實現。
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
字串,然後讀取回來在載入到記憶體中。不過,簡單的範例也夠說明問題了。
這是類似於一種最上層異常捕獲的機制,在程式的入口處捕獲所有的異常。
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)
}
}
註釋寫的很清楚,聰明的你一看就懂。
與 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
結尾,很清爽的感覺。
這也是一個很常用的知識點。這裡有兩種方式:
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)
}
我建議使用第二種,更便捷自帶型別轉換,還可以給預設值,非常好。
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
中也有類似的回撥的介面(我忘了名字)。
作為一門高階語言,反射肯定是有的。還是使用 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()
}
沒什麼好說的,寫程式碼全靠猜。
類似 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()
}
真的是特別簡單。
類似於ConcurrentHashMap
,與普通的 api
有所不同。
var sessions = sync.Map{}
sessions.Store(uuid, uuid)
load, ok := sessions.Load(value.Token)
if ok {
// 做你想做的事情
}
這裡就是函數式變成的例子了。函數是一等公民可以作為引數隨意傳遞。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 "我是匿名函數的返回值"
}
}
比如這裡,如果要初始化紀錄檔什麼。最後需要讓框架在哪裡列印紀錄檔,就需要將這個初始化的紀錄檔範例傳遞過去。總而言之,言而總之。會需要讓程式碼各種傳遞。
這種方式在於第一次呼叫的時候會執行上面的程式碼片段,後面只是儲存了這個函數的控制程式碼,然後可以一直呼叫這個匿名函數。
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)
}
}
作用:在不同的協程中傳遞上下文。
這是最高頻率的操作了,使用任何語言都無法錯過。
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
包。
如果說使用 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
的經驗總結,文中難免有錯誤,懇請指正。
作者:京東零售 付偉
來源:京東與開發者社群