後端
為什麼 Golang 適合做為網頁後端程式的語言呢?
由於 Golang 有以下的優點:
但 Golang 並非完美無缺,以下是要考量的點:
--src 放置專案的原始碼檔案
--pkg 放置編譯後生成的包 / 庫檔案
--bin 放置編譯後生成的可執行檔案
go mod init 初始化
go mod tidy 整理模組
go get github.com/gin-gonic/gin
go get github.com/go-sql-driver/mysql
main.go
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run(":8000")
}
mysql.go
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" //只引用該套件的init函數
)
func main() {
db, err := sql.Open("mysql", "root:root@tcp(mysql)/test?charset=utf8")
defer db.Close()
//插入資料,使用預處理避免發生injection
stmt, err := db.Prepare("INSERT userinfo SET username=?,department=?,created=?")
checkErr(err)
_, err = stmt.Exec("astaxie", "研發部門", "2012-12-09")
checkErr(err)
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
兩者都為 package main 代表他們本質上是一隻程式 只是分為不同檔案
不同的package之間需分為不同資料夾,可做非循環引用: "module_name/floder_name" ex.import router(別名) "main/routes"
Go 實作 Restful API
Go的資料類別一共分為四大類:
var a // 不定型別的變數
var a int // 宣告成 int
var msg string // 宣告成 string
var a int = 10 // 初始化同時宣告
var a = 10 // 會自動幫你判定為整數型別
var a, b int // a 跟 b 都是 intvar a, b = 0
var a int , b string
var a, b, c int = 1, 2, 3
var a, b, c = 1, 2, 3
var(
a bool = false // 記得要不同行,不然會錯
b int
c = "hello"
)
// 在函數中,「:=」 簡潔賦值語句在明確類型的地方,可以替代 var 定義。
//「:=」 結構不能使用在函數外,函數外的每個語法都必須以關鍵字開始。
// := 只能用在宣告
var msg = "Hello World"
等於
msg := "Hello World" //自動判定型態
a := 0 // int
a, b, c := 0, true, "tacolin" // 這樣就可以不同型別寫在同一行
_, b := 34, 35 // _(下劃線)是個特殊的變數名,任何賦予它的值都會被丟棄。
在Go中
bool 與 int 不能直接轉換,true
,false
不直接等於 1 與 0
型態 | 描述 |
---|---|
int8 | 8-bit signed integer |
int16 | 16-bit signed integer |
int32 | 32-bit signed integer |
int64 | 64-bit signed integer |
uint8 | 8-bit unsigned integer |
uint16 | 16-bit unsigned integer |
uint32 | 32-bit unsigned integer |
uint64 | 64-bit unsigned integer |
int | Both in and uint contain same size, either 32 or 64 bit. |
uint | Both in and uint contain same size, either 32 or 64 bit. |
rune | 等價 unit32 ,表示一個Unicode字符 |
byte | 等價 uint8 ,表示一個ASCII字符 |
uintptr | It is an unsigned integer type. Its width is not defined, but its can hold all the bits of a pointer value. |
型態 | 描述 |
---|---|
float32 | 32-bit IEEE 754 floating-point number |
float64 | 64-bit IEEE 754 floating-point number |
型態 | 描述 |
---|---|
complex64 | Complex numbers which contain float32 as a real and imaginary component. |
complex128 | Complex numbers which contain float64 as a real and imaginary component. |
var mystr01 string = "\\r\\n"
等於
var mystr02 string = `\r\n`
輸出:\r\n
``
表示一個多行的字串
// 第一種方法
var arr = [3]int{1,2,3} //%T = [3]int
// 第二種方法
arr := [3]int{1,2,3}
// 第三種方法
arr := [...]int{1,2,3} // 可以省略長度而採用`...`的方式,Go 會自動根據元素個數來計算長度
//注意類型為字串時
var arr = [3]string{
"first",
"second",
"third", //最後這裡要有逗號
}
為一個左閉右開的結構
//宣告一個空的切片
var arr []int //默認值為nil
運用make( []Type, size, cap )
指定類型、長度、容量,
建立一個容量為10,目前長度為3的切片:
make([]int, 3, 10) //make( []Type, size, cap )
arr[0:2]
//-->[1 2] 結尾索引不算在內
myarr := []int{1}
// 追加一個元素
myarr = append(myarr, 2)
// 追加多個元素
myarr = append(myarr, 3, 4)
// 追加一個切片, ... 表示解包,不能省略
myarr = append(myarr, []int{7, 8}...)
// 在開頭插入元素0
myarr = append([]int{0}, myarr[0:]...) //[0:]為開頭的話可省略
// 在中間插入一個切片(兩個元素)
myarr = append(myarr[:5], append([]int{5, 6}, myarr[5:]...)...)
fmt.Println(myarr) //--> [0 1 2 3 4 7 8]
slice1 := []int{1,2,3}
slice2 := make([]int, 2)
copy(slice2, slice1)
fmt.Println(slice1, slice2)
// 由於slice2容量只有2所以只有slice1[0:2]被複製過去
// 輸出結果: [1 2 3] [1 2]
// 第一種方法
var scores map[string]int = map[string]int{"english": 80, "chinese": 85}
// 第二種方法
scores := map[string]int{"english": 80, "chinese": 85}
// 第三種方法
scores := make(map[string]int)
scores["english"] = 80
scores["chinese"] = 85
scores["math"] = 95
scores["math"] = 100 //若已存在,直接更新
delete( scores, "math" )
fmt.Println(scores["math"]) //不存在則返回value-type的0值
//-->100
elements := map[string]string{
"H": "Hydrogen",
"He": "Helium",
"Li": "Lithium",
"Be": "Beryllium"
}
value, isExist := elements["H"];
// value = Hydrogen, isExist = true
value, isExist := elements["A"];
// value = "", isExist = false
elements := map[string]map[string]string{
"H": map[string]string{
"name":"Hydrogen",
"state":"gas",
},
"He": map[string]string{
"name":"Helium",
"state":"gas",
},
"Li": map[string]string{
"name":"Lithium",
"state":"solid",
},
"Be": map[string]string{
"name":"Beryllium",
"state":"solid",
},
"B": map[string]string{
"name":"Boron",
"state":"solid",
},
"C": map[string]string{
"name":"Carbon",
"state":"solid",
},
"N": map[string]string{
"name":"Nitrogen",
"state":"gas",
},
"O": map[string]string{
"name":"Oxygen",
"state":"gas",
},
"F": map[string]string{
"name":"Fluorine",
"state":"gas",
},
"Ne": map[string]string{
"name":"Neon",
"state":"gas",
},
}
if el, ok := elements["Li"]; ok {
fmt.Println(el["name"], el["state"])
}
自定義型別,struct裡可以放struct型別的物件
參考資料
type person struct {
name string
height int
}
type Message struct {
Sender string `json:"sender"`
RoomId string `json:"roomId"`
Content string `json:"content"`
Time string `json:"time"`
}
[]byte
格式的 json 資料jsonMessage, _ := json.Marshal(&Message{Sender: c.id, RoomId: c.roomId, Content: string(message), Time: time})
var msg Message
json.Unmarshal(message, &msg)
跟C語言一樣,Go語言也有指標。
func zero( x *int ) {
*x = 0
}
func main() {
x := 5
zero( &x )
fmt.Println( x )
}
package main
import "fmt"
import "math"
type geometry interface {
area() float64
perimeter() float64
}
type square struct {
width, height float64
}
type circle struct {
radius float64
}
func (s square) area() float64 {
return s.width * s.height
}
func (s square) perimeter() float64 {
return 2*s.width + 2*s.height
}
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) perimeter() float64 {
return 2 * math.Pi * c.radius
}
func measure(g geometry) {
fmt.Println(g)
fmt.Println(g.area())
fmt.Println(g.perimeter())
}
func main() {
s := square{width: 3, height: 4}
c := circle{radius: 5}
measure(s)
measure(c)
}
Go只有一種迴圈關鍵字,就是for
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
}
跟 C 或者 Java 中一樣,可以讓前置、後置語句為空。
func main() {
sum := 1
for ; sum < 1000; {
sum += sum
}
fmt.Println(sum)
}
基於此可以省略分號:C 的 while 在 Go 中叫做 「for」。
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}
如果省略了迴圈條件,迴圈就不會結束,因此可以用更簡潔地形式表達無窮迴圈。
func main() {
for {
fmt.Println("Hello World")
}
}
可以這樣尋訪
var x [4]float64{ 23, 45, 33, 21 }
var total float64 = 0
for i := 0; i < 4; i++ {
total += x[i]
}
fmt.Println( total / float64(4))
使用len
獲取陣列元素數量
var x [4]float64{ 23, 45, 33, 21 }
var total float64 = 0
for i := 0; i < len(x); i++ {
total += x[i]
}
fmt.Println( total / float64(len(x)))
更精簡一點
var x [4]float64{ 23, 45, 33, 21 }
var total float64 = 0
for i, value := range x {
total += value
}
fmt.Println( total / float64(len(x)))
for迴圈前面的第一個變數意義為陣列索引(index),而後面變數代表該索引值所代表的陣列值。以上寫法會出錯,由於Go不允許沒有使用的變數出現在程式碼中,迴圈的i變數我們使用佔位符(_)替代。
func main() {
var x [4]float64{ 23, 45, 33, 21 }
var total float64 = 0
for _, value := range x {
total += value
}
fmt.Println( total / float64(len(x)))
}
可以利用break提前退出循環。
func main() {
for i := 0; i < 10; i++ {
if i > 5 {
break
}
fmt.Println(i)
}
}
如果有多重迴圈,可以指定要跳出哪一個迴圈,但需要指定標籤。
func main() {
outer: // 標籤在此
for j := 0; j < 5; j++ {
for i := 0; i < 10; i++ {
if i > 6 {
break outer
}
fmt.Println(i)
}
}
}
continue忽略之後的程式碼,直接執行下一次迭代。
func main() {
for i := 1; i <= 10; i++ {
if i < 6 {
continue
}
fmt.Println(i)
}
}
同樣的如果有多重迴圈,也可以指定標籤。
func main() {
outer: // 標籤在此
for i := 1; i < 10; i++ {
for j := 1; j < 10; j++ {
if i == j {
continue outer
}
fmt.Println( "i: ", i, " j: ", j );
}
}
}
Go 語言跟 C 語言一樣也有「 goto 」,但是不建議使用,會讓程式的結構變得很糟糕。
func main() {
i := 0
HERE:
fmt.Print(i)
i++
if i < 10 {
goto HERE
}
}
此範例文章取自openhome.cc
就許多現代語言而言,例外處理機制是基本特性之一,然而,例外處理是好是壞,一直以來存在著各種不同的意見,在 Go 語言中,沒有例外處理機制,取而代之的,是運用 defer、panic、recover 來滿足類似的處理需求。
在 Go 語言中,可以使用 defer 指定某個函式延遲執行,那麼延遲到哪個時機?簡單來說,在函式 return語句之後準備返回呼叫的函式之前,例如:
func myfunc() {
fmt.Println("B")
}
func main() {
defer myfunc()
fmt.Println("A")
}
輸出
A
B
package main
import "fmt"
func Triple(n int) (r int) {
defer func() {
r += n // 修改返回值
}()
return n + n // <=> r = n + n; return
}
func main() {
fmt.Println(Triple(5))
}
輸出
15
func main() {
name := "go"
defer fmt.Println(name) // 變數name的值被記住了,所以會输出go
name = "python"
fmt.Println(name) // 输出: python
}
輸出
python
go
package main
import "fmt"
func deferredFunc1() {
fmt.Println("deferredFunc1")
}
func deferredFunc2() {
fmt.Println("deferredFunc2")
}
func main() {
defer deferredFunc1()
defer deferredFunc2()
fmt.Println("Hello, 世界")
}
// 輸出結果:
Hello, 世界
deferredFunc2
deferredFunc1
func f() {
r := getResource() //0,獲取資源
......
if ... {
r.release() //1,釋放資源
return
}
......
if ... {
r.release() //2,釋放資源
return
}
......
r.release() //3,釋放資源
return
}
使用 defer 後,不論在哪 return 都會執行 defer 後方的函數,如此便不用在每個return前寫上r.release()
func f() {
r := getResource() //0,獲取資源
defer r.release() //1,釋放資源
......
if ... {
...
return
}
......
if ... {
...
return
}
......
return
}
以下是清除資源的範例:
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("/tmp/dat")
if err != nil {
fmt.Println(err)
return;
}
defer func() { // 延遲執行,而且函式 return 後一定會執行
if f != nil {
f.Close()
}
}()
b1 := make([]byte, 5)
n1, err := f.Read(b1)
if err != nil {
fmt.Printf("%d bytes: %s\n", n1, string(b1))
// 處理讀取的內容....
}
}
如果在函式中執行 panic,那麼函式的流程就會中斷,若 A 函式呼叫了 B 函式,而 B 函式中呼叫了 panic,那麼 B 函式會從呼叫了 panic 的地方中斷,而 A 函式也會從呼叫了 B 函式的地方中斷,若有更深層的呼叫鏈,panic 的效應也會一路往回傳播。
package main
import (
"fmt"
"os"
)
func check(err error) {
if err != nil {
panic(err)
}
}
func main() {
f, err := os.Open("/tmp/dat")
check(err)
defer func() {
if f != nil {
f.Close()
}
}()
b1 := make([]byte, 5)
n1, err := f.Read(b1)
check(err)
fmt.Printf("%d bytes: %s\n", n1, string(b1))
}
如果在開啟檔案時,就發生了錯誤,假設這是在一個很深的呼叫層次中發生,若你直接想撰寫程式,將 os.Open 的 error 逐層傳回,那會是一件很麻煩的事,此時直接發出 panic,就可以達到想要的目的。
如果發生了 panic,而你必須做一些處理,可以使用 recover,這個函式必須在被 defer 的函式中執行才有效果,若在被 defer 的函式外執行,recover 一定是傳回 nil。
如果有設置 defer 函式,在發生了 panic 的情況下,被 defer 的函式一定會被執行,若當中執行了 recover,那麼 panic 就會被捕捉並作為 recover 的傳回值,那麼 panic 就不會一路往回傳播,除非你又呼叫了 panic。
因此,雖然 Go 語言中沒有例外處理機制,也可使用 defer、panic 與 recover 來進行類似的錯誤處理。例如,將上頭的範例,再修改為:
package main
import (
"fmt"
"os"
)
func check(err error) {
if err != nil {
panic(err)
}
}
func main() {
f, err := os.Open("/tmp/dat")
check(err)
defer func() {
if err := recover(); err != nil {
fmt.Println(err) // 這已經是頂層的 UI 介面了,想以自己的方式呈現錯誤
}
if f != nil {
if err := f.Close(); err != nil {
panic(err) // 示範再拋出 panic
}
}
}()
b1 := make([]byte, 5)
n1, err := f.Read(b1)
check(err)
fmt.Printf("%d bytes: %s\n", n1, string(b1))
}
if 條件一 {
分支一
} else if 條件二 {
分支二
} else if 條件 ... {
分支 ...
} else {
分支 else
}
// { 必須與if..在同一行
&&
: 且
||
: 或
在 if 裡允許先運行一個表達式,取得變數後再來做判斷:
func main() {
if age := 20;age > 18 {
fmt.Println("已成年")
}
}
與一般的switch宣告方法一樣,條件不能重複
import "fmt"
func main() {
month := 2
switch month {
case 3, 4, 5:
fmt.Println("春天")
case 6, 7, 8:
fmt.Println("夏天")
case 9, 10, 11:
fmt.Println("秋天")
case 12, 1, 2:
fmt.Println("冬天")
default:
fmt.Println("輸入有誤...")
}
}
import "fmt"
// 判断一个同学是否有挂科记录的函数
// 返回值是布尔类型
func getResult(args ...int) bool {
for _, i := range args {
if i < 60 {
return false
}
}
return true
}
func main() {
chinese := 80
english := 50
math := 100
switch getResult(chinese, english, math) {
// case 后也必须 是布尔类型
case true:
fmt.Println("该同学所有成绩都合格")
case false:
fmt.Println("该同学有挂科记录")
}
}
fallthrough
可以往下穿透一層,執行下一個case語句且不用判斷條件,但其必須為該case的最後一個語句,否則會錯誤func add( x int, y int ) int {
return x + y
}
func main() {
fmt.Println( add( 42, 13 ) )
}
當兩個或多個連續的函數命名參數是同一類型,則除了最後一個類型之外,其他都可以省略。
所以如果參數的型態都一樣的話,可以精簡為:
func add( x, y int ) int {
return x + y
}
func main() {
fmt.Println( add( 42, 13 ) )
}
函數可以返回任意數量的返回值,這個函數返回了兩個字串。
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}
// 輸出結果 world hello
在 Go 中,函數可以返回多個「結果參數」,而不僅僅是一個值。它們可以像變數那樣命名和使用。
如果命名了返回值參數,一個沒有參數的 return 語句,會將當前的值作為返回值返回。以這個程式碼為例,sum int 表示宣告整數 sum ,將參數 17 放入 sum 中,x, y int 宣告整數 x,y 在下面使用,由於 return 沒有設定返回值,這邊程式就將 x,y 都回傳了,所以結果會出現 7 10。
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
func main() {
fmt.Println(split(17))
}
要使用Goroutine只要在呼叫的函數前面加一個go關鍵字即可
package main
import "fmt"
func f(n int) {
for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
}
}
func main() {
go f(0)
}
執行後會發現什麼東西都沒有印出,因為 goroutine 是平行處理的,
所以在還沒開始印 n 之前 main 這個主要的函式已經結束了。
使用內建的 time 函式讓 main 函式等 goroutine 先跑完。
package main
import (
"fmt"
"time"
)
func f(n int) {
for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
}
}
func main() {
go f(0)
time.Sleep(time.Second * 1) // 暫停一秒鐘
}
此龜兔賽跑範例文章引用自openhome.cc/Go/Goroutine
先來看個沒有啟用 Goroutine,卻要寫個龜兔賽跑遊戲的例子,你可能是這麼寫的:
package main
import (
"fmt"
"math/rand"
"time"
)
func random(min, max int) int {
rand.Seed(time.Now().Unix())
return rand.Intn(max-min) + min
}
func main() {
flags := [...]bool{true, false}
totalStep := 10
tortoiseStep := 0
hareStep := 0
fmt.Println("龜兔賽跑開始...")
for tortoiseStep < totalStep && hareStep < totalStep {
tortoiseStep++
fmt.Printf("烏龜跑了 %d 步...\n", tortoiseStep)
isHareSleep := flags[random(1, 10)%2]
if isHareSleep {
fmt.Println("兔子睡著了zzzz")
} else {
hareStep += 2
fmt.Printf("兔子跑了 %d 步...\n", hareStep)
}
}
}
由於程式只有一個流程,所以只能將烏龜與兔子的行為混雜在這個流程中撰寫,而且為什麼每次都先遞增烏龜再遞增兔子步數呢?這樣對兔子很不公平啊!如果可以撰寫程式再啟動兩個流程,一個是烏龜流程,一個兔子流程,程式邏輯會比較清楚。
你可以將烏龜的流程與兔子的流程分別寫在一個函式中,並用 go 啟動執行:
package main
import (
"fmt"
"math/rand"
"time"
)
func random( min, max int ) int {
rand.Seed( time.Now().Unix() )
return rand.Intn( max - min ) + min
}
func tortoise( totalStep int ) {
for step := 1; step <= totalStep; step++ {
fmt.Printf( "烏龜跑了 %d 步...\n", step )
}
}
func hare(totalStep int) {
flags := [...]bool{true, false}
step := 0
for step < totalStep {
isHareSleep := flags[random(1, 10)%2]
if isHareSleep {
fmt.Println("兔子睡著了zzzz")
} else {
step += 2
fmt.Printf("兔子跑了 %d 步...\n", step)
}
}
}
func main() {
totalStep := 10
go tortoise(totalStep)
go hare(totalStep)
time.Sleep(5 * time.Second) // 給予時間等待 Goroutine 完成
}
有沒有辦法知道 Goroutine 執行結束呢?實際上沒有任何方法可以得知,除非你主動設計一種機制,可以在 Goroutine 結束時執行通知,使用 Channel 是一種方式,這在之後的文件再說明,這邊先說明另一種方式,也就是使用 sync.WaitGroup。
sync.WaitGroup 可以用來等待一組 Goroutine 的完成,主流程中建立 sync.WaitGroup,並透過 Add 告知要等待的 Goroutine 數量,並使用 Wait 等待 Goroutine 結束,而每個 Goroutine 結束前,必須執行 sync.WaitGroup 的 Done 方法。
因此,我們可以使用 sync.WaitGroup 來改寫以上的範例:
package main
import (
"fmt"
"math/rand"
"time"
"sync"
)
func random( min, max int ) int {
rand.Seed( time.Now().Unix() )
return rand.Intn( max - min ) + min
}
func tortoise( totalStep int, wg *sync.WaitGroup ) {
defer wg.Done()
for step := 1; step <= totalStep; step++ {
fmt.Printf( "烏龜跑了 %d 步...\n", step )
}
}
func hare(totalStep int, wg *sync.WaitGroup ) {
defer wg.Done()
flags := [...]bool{true, false}
step := 0
for step < totalStep {
isHareSleep := flags[random(1, 10)%2]
if isHareSleep {
fmt.Println("兔子睡著了zzzz")
} else {
step += 2
fmt.Printf("兔子跑了 %d 步...\n", step)
}
}
}
func main() {
wg := new( sync.WaitGroup )
wg.Add( 2 )
totalStep := 10
go tortoise( totalStep, wg )
go hare( totalStep, wg )
time.Sleep(5 * time.Second) // 給予時間等待 Goroutine 完成
}
通過 Channel 可以讓 goroutine 之間通信
ch_name := make(chan <TYPE>{,NUM}) //類型與大小
Ch <- DATA
DATA := <- Ch
func main() {
messages := make(chan string)
go func() { messages <- "ping" }()
msg := <- messages
fmt.Println( msg )
}
透過這個方法就可以簡單的讓 Goroutine 可以溝通
有一個類似 Switch 的流程控制「Select」,它只能應用於 Channel
package main
import "time"
import "fmt"
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(time.Second * 1)
c1 <- "one"
}()
go func() {
time.Sleep(time.Second * 2)
c2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}
強制編碼風格
Go語言為了讓團隊開發能夠更加的簡單,他統一了程式碼的風格,如果沒有遵照他的規範寫的話,你再如何編譯都不會成功。
以下為錯誤的程式碼風格
package main
import "fmt"
func main()
{
i:= 1
fmt.Println("Hello World", i)
}
如果你左右括弧的寫法是像上面那樣,你將會看到下列的錯誤訊息
syntax error: unexpected semicolon or newline before {non-declaration
statement outside function body syntax error: unexpected }
以下為正確的寫法
package main
import "fmt"
func main() {
i:= 1
fmt.Println("Hello World", i)
}
為了保持程式碼的乾淨,你宣告了一個變數,但是卻沒有使用,Go 語言連編譯都不會讓你編譯。舉例來說,變數 i 並沒有被使用。
package main
import "fmt"
func main() {
i := 1
fmt.Println("Hello World i")
}
你會出現下列錯誤訊息
# command-line-arguments
./test.go:6:2: i declared but not used
非強制性編譯風格建議
以下程式碼可以正常的編譯,但是很醜不好閱讀。
package main
import "fmt"
func main() { i:= 1
fmt.Println("Hello World", i)}
我們可以利用go fmt
指令幫忙整理程式碼編譯格式。
用法
go fmt <filename>.go # 整理某個檔案
go fmt *.go # 整理目錄下所有go檔案
go fmt # 同上
如果程式碼不需要調整他不會出現任何訊息,成功會出現你使用的程式檔名。
格式化工具幫你做到了下列事情:
Go之所以會那麼簡潔,是因為它有一些預設的行為:
官方 - Quick start
範例 - Hello ,gRPC
How we use gRPC to build a client/server system in Go
比起 JSON 更方便、更快速、更簡短的 Protobuf 格式
API 文件就是你的伺服器,REST 的另一個選擇:gRPC
比較 gRPC 服務與 HTTP API
同時提供HTTP接口
gRPC-Web:envoy
如果兩邊都想要 - gRPC Gateway
[1] Go (Golang) 適合初學者做為網頁後端程式嗎?
[2] Golang — GOROOT、GOPATH、Go-Modules-三者的關係介紹
[3] GeeksforGeeks: Data Types in Go
[4] 初學Golang30天
[5] Go 语言设计与实现 - make 和 new
[6] Opencc Go
[7] 使用 Golang 打造 Web 應用程式
[8] 五分钟理解golang的init函数
[9] Go标准库:Go template用法详解
[10] How to use local go modules with golang with examples
[11] Go并发编程模型:主动停止goroutine
[12] Go gin框架入门教程
[13] Golang 套件初始化流程
[14] Go語言變數的生命週期
[15] 使用golang的mysql无法插入emoji表情的问题
[16] Go语言高级编程(Advanced Go Programming)
[17] Golang中range的使用方法及注意事项
[18] Go語言101
gorilla/websocket - example:chat
Build a Realtime Chat Server With Go and WebSockets
Go Websocket 長連線