前言
本文使用代碼片段的形式來解釋在 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
均為檔案夾,也可以說是 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
包,
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=https://www.cnblogs.com/jingdongkeji/archive/2023/06/06/%d/n", key, value)
}
for _, value := range m2 {
fmt.Printf("m2:value=https://www.cnblogs.com/jingdongkeji/archive/2023/06/06/%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 = https://www.cnblogs.com/jingdongkeji/archive/2023/06/06/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 = https://www.cnblogs.com/jingdongkeji/archive/2023/06/06/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=https://www.cnblogs.com/jingdongkeji/archive/2023/06/06/%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
的經驗總結,文中難免有錯誤,懇請指正,
作者:京東零售 付偉
來源:京東與開發者社區
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/554478.html
標籤:其他
下一篇:返回列表