【Golang 1.18】Let' Go, MySQL我來了…
同學們上課啦,記得當時年紀小,筆者當時流行的是網頁工程師-前/後端,但是工作非常的難找,因為Web技術已經發展20多年了,所以會的人太多了,而薪資也早已進入了大紅海時代;後來看到手機系統的出現,一不小心就轉職到了iOS打字工的行列之中;但是,工作之後發現,老舊的API寫法相當的不適合在手機端使用,要改嘛,又不是件容易的事(),有問題第一個顯示的也一定是在前端被發現,還是靠自己最好…資深員工很難溝通?
作業環境
項目 | 版本 |
---|---|
CPU | Apple M1 |
macOS | Big Sur 12.3 arm64 |
Golang | 1.18 arm64 |
Visual Studio Code | 1.66 arm64 |
Postman | 9.0.9 arm64 |
MySQL | 8.0.28 arm64 |
事前準備
安裝
brew install go
brew install mysql
重複使用Code
- 地球只有一個,為了環保愛地球,Code當然能重用就重用,能不寫就不寫,您說是吧?
- 其實Go的套件使用,有一個很重要的分水嶺 v1.11 - GO111MODULE,簡單來說就是以前的很難用,現在比較人性化了…
package main
import (
"fmt"
)
func main() {
nextCountTest()
}
// 測試用
func nextCountTest() {
nextInt := counter()
fmt.Println(nextInt())
fmt.Println(nextInt())
fmt.Println(nextInt())
}
// 記數器 => 想留下來以後用
func counter() func() int {
index := 0
return func() int {
index++
return index
}
}
Go Module
go mod init william
- 現在就來分類吧…
// go.mod
module william
go 1.18
// main.go
package main
import (
"fmt"
util "william/utility" // 以go.mod上的module為root的相對位置
)
func main() {
nextCountTest()
}
// 計數器測試
func nextCountTest() {
nextInt := util.Counter()
fmt.Println(nextInt())
fmt.Println(nextInt())
fmt.Println(nextInt())
}
package utility
// 記數器 (大寫: publilc / 小寫: privete)
func Counter() func() int {
index := 0
return func() int {
index++
return index
}
}
- 當然,也可以裝別人寫的喲…其實go.mod是很像是npm中的package.json / cocoapods的Podfile設定檔…
- 現在我們來安裝第三方的MySQL Driver
go get -u github.com/go-sql-driver/mysql // 安裝
go get -u github.com/go-sql-driver/mysql@none // 移除 => @none
WebAPI
Gin - Web Framework
- 終於進入重頭戲了,安裝Gin - Web Framework => 琴酒
go get -u github.com/gin-gonic/gin
建立資料庫 + 資料表
Create Database eBook;
Use eBook;
Create Table GoBook(
id bigint(20) PRIMARY KEY AUTO_INCREMENT NOT NULL,
isbn bigint(20),
name char(255),
date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Show Tables;
第一支API
package main
import (
"net/http"
util "william/utility" // 以go.mod上的module為root的相對位置
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
)
func main() {
router := gin.Default()
router.MaxMultipartMemory = 8 << 20 // 8 MiB
userName(router)
router.Run(util.Post)
}
// 簡單的GET測試 => http://localhost:8080/user/William/Welcome
func userName(router *gin.Engine) {
router.GET("/user/:name/*action", func(context *gin.Context) {
name := context.Param("name")
action := context.Param("action")
message := name + " is " + action
context.String(http.StatusOK, message)
})
}
新增資料 - Insert
// main.go
package main
import (
"fmt"
util "william/utility" // 以go.mod上的module為root的相對位置
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, error := util.ConnentDatabase()
if error != nil {
fmt.Println(error)
return
}
router := gin.Default()
router.MaxMultipartMemory = 8 << 20 // 8 MiB
util.InsertBook(router, db)
router.Run(":8080")
}
// utility/util.go
package utility
import (
"database/sql"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
const (
address = "localhost"
port = 3306
admin = "root"
password = "12345678"
databaseType = "mysql"
database = "eBook"
table = "GoBook"
)
// 資料庫連線 => root:12345678@tcp(localhost:3306)/eBook?charset=utf8&loc=Asia%2FShanghai&parseTime=true
func ConnentDatabase() (*sql.DB, error) {
dataSource := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", admin, password, address, port, database)
dataSource += "?charset=utf8&loc=Asia%2FShanghai&parseTime=true"
database, error := sql.Open(databaseType, dataSource)
database.SetConnMaxLifetime(time.Duration(10) * time.Second)
database.SetMaxOpenConns(10)
database.SetMaxIdleConns(10)
return database, error
}
// 新增書籍 => http://localhost:8080/book/9789861079493 + form-data: name = 棋魂完全版20
func InsertBook(router *gin.Engine, db *sql.DB) {
uri := "/book/:isbn"
router.POST(uri, func(context *gin.Context) {
isbn, _ := stringToInt(context.Param("isbn")) // 取url上的參數 => context.Param()
name := context.PostForm("name") // 取form上的參數 => context.PostForm()
sql := "Insert Into GoBook(isbn, name) values(?, ?)"
result, error := db.Exec(sql, isbn, name)
contextJSON(context, http.StatusOK, result, error)
})
}
// utility/tools.go
package utility
import (
"strconv"
"github.com/gin-gonic/gin"
)
// 文字 => 數字
func stringToInt(str string) (int, error) {
return strconv.Atoi(str)
}
// 輸出JSON
func contextJSON(context *gin.Context, httpStatus int, result interface{}, error error) {
context.JSON(httpStatus, gin.H{
"error": error,
"result": result,
})
}
搜尋資料 - Select
- 使用db.Query()來搜尋資料
- 因為是GET,所以直接使用網頁執行也可以…
- 搜尋完記得要關掉喲…
// 建立GoBook的資料結構
type GoBook struct {
ID int `json:"id"` // <欄位> <類型> <真實在json輸出的欄位名稱>
ISBN int `json:"isbn"` // ISBN是數字,在json叫isbn
Name string `json:"name"`
CreateTime time.Time `json:"date"`
}
// <Get>搜尋書籍 => http://localhost:8080/book
func SelectBooks(router *gin.Engine, db *sql.DB) {
uri := "/book"
router.GET(uri, func(context *gin.Context) {
books := []GoBook{}
sql := "Select * From GoBook"
rows, error := db.Query(sql)
defer func() {
rows.Close()
}()
for rows.Next() {
var book GoBook
rows.Scan(&book.ID, &book.ISBN, &book.Name, &book.CreateTime) // 要注意的是使用指標
books = append(books, book)
}
contextJSON(context, http.StatusOK, books, error)
})
}
更新資料 - Select
// <PUT>修改某一本書籍 => http://localhost:8080/book/9789861079493
func UpdateBook(router *gin.Engine, db *sql.DB) {
uri := "/book/:isbn"
router.PUT(uri, func(context *gin.Context) {
isbn, _ := stringToInt(context.Param("isbn")) // 取url上的參數 => context.Param()
rawJSON, _ := ioutil.ReadAll(context.Request.Body) // 讀取RawBody上的值
sql := fmt.Sprintf("Update %s Set name=? Where isbn=?", table)
dictionary := RawJSONToMap(rawJSON)
name := dictionary["name"]
result, error := db.Exec(sql, name, isbn)
contextJSON(context, http.StatusOK, result, error)
})
}
刪除資料 - Delete
// <Delete>刪除某一本書籍 => http://localhost:8080/book/9789861079493
func DeleteBook(router *gin.Engine, db *sql.DB) {
uri := "/book/:isbn"
router.DELETE(uri, func(context *gin.Context) {
var error error = nil
var count int64 = 0
var isSuccess bool = false
defer func() {
result := map[string]interface{}{"isSuccess": isSuccess, "count": count}
contextJSON(context, http.StatusOK, result, error)
}()
isbn, error := stringToInt(context.Param("isbn"))
if error != nil { return }
sql := fmt.Sprintf("Delete From %s Where isbn=?", table)
result, error := db.Exec(sql, isbn)
if error != nil { return }
rowsaffected, error := result.RowsAffected() // 很嚴謹的寫法會判斷RowsAffected()是否與處理的資料筆數一致
if error != nil { return }
isSuccess = true
count = rowsaffected
})
}
檔案上傳 / 下載
// 檔案上傳 => multipart/form-data, default is 32 MiB / file=<base64>
func UploadFile(router *gin.Engine, db *sql.DB) {
key := "file"
folder := "/Users/william.weng/Desktop/"
router.POST("/upload", func(context *gin.Context) {
var error error = nil
var isSuccess bool = false
var filename = uuid.NewString() + ".jpg"
defer func() {
result := map[string]interface{}{"isSuccess": isSuccess}
contextJSON(context, http.StatusOK, result, error)
}()
filePath := folder + filename
file, error := context.FormFile(key)
if error != nil { return }
error = context.SaveUploadedFile(file, filePath)
if error != nil { return }
isSuccess = true
})
}
func main() {
router := gin.Default()
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.Static("static", "./static") // http://localhost:8080/static <=> ./static
router.Run(":8080")
}
iOS 推播功能
- 就是所謂的APNS + .p8憑證 + Go的APNS2套件
go get -u github.com/sideshow/apns2
// iOS推播功能
func pushNotification(context *gin.Context, deviceToken string) (*apns2.Response, error) {
const authKeyP8 = "./<AuthKey>.p8"
const keyId = "<KeyId>"
const teamID = "<TeamID>"
const topic = "idv.william.Example"
var authKey *ecdsa.PrivateKey = nil
var response *apns2.Response = nil
var error error = nil
payload := []byte(`{"aps":{"alert":"Golang,愛你喲…"}}`)
authKey, error = token.AuthKeyFromFile(authKeyP8)
if error != nil {
return response, error
}
notification := &apns2.Notification{}
notification.Topic = topic
notification.DeviceToken = deviceToken
notification.Payload = payload
token := &token.Token{
AuthKey: authKey,
KeyID: keyId,
TeamID: teamID,
}
client := apns2.NewTokenClient(token).Development()
response, error = client.Push(notification)
if error != nil {
return response, error
}
return response, error
}
Android 推播功能
// Android推播功能
func firebaseCloudMessaging(token string) (*fcm.Response, error) {
apiKey := "<YourProductApiKey>"
message := &fcm.Message{
To: token,
Data: map[string]interface{}{
"foo": "bar",
},
Notification: &fcm.Notification{
Title: "Golang",
Body: "Golang,愛你喲…",
},
}
client, error := fcm.NewClient(apiKey)
if error != nil {
return nil, error
}
return client.Send(message)
}
範例程式碼下載