天天看點

Golang學習筆記(二):第一個可用于生産環境下的真正Go程式1. 緣起2. 再來一個Hello World,但這次是個Web站點!3 真正的生産環境站點……4 結語

Golang學習筆記(二):第一個可用于生産環境下的真正Go程式

  • 1. 緣起
  • 2. 再來一個Hello World,但這次是個Web站點!
    • 2.1 編寫代碼
    • 2.2 執行代碼
    • 2.3 代碼解析
  • 3 真正的生産環境站點……
    • 3.1 項目代碼及詳細注釋
      • 3.1.1 項目目錄結構
      • 3.1.2 主程式demoweb.go
      • 3.1.3 日志及錯誤處理子產品logger.go
      • 3.1.4 配置檔案讀取子產品config.go
      • 3.1.5 資料庫操作子產品
      • 3.1.6 前端Html代碼
      • 3.1.7 MySQL表結構
    • 3.2 執行程式
      • 3.2.1 go.mod
      • 3.2.2 執行
  • 4 結語

1. 緣起

在前一篇文章《Golang學習筆記(一):緣起及一個不一樣的HelloWorld》中我們對Go開發環境有了一個最初步的認識,HolloWorld是程式設計語言共同預設的第一個可執行代碼,然并卵!我們老祖宗教育我們要“學以緻用”!是以,我們一開始就要考慮怎麼在實際生産環境下的用Go。大家程式設計的主要目的開發網絡應用提供網絡服務,剛好,Go有一個功能強大使用簡單的net/http包,簡單的幾行代碼就能建構一個性能超越用Apache、Tomcat建構的網站。這麼比有點奇怪,怎麼把Go語言和Web server一起比較了:),原因是net/http包提供HTTP用戶端和伺服器實作。下面我們就開始吧……

2. 再來一個Hello World,但這次是個Web站點!

實作一個最簡單HTTP server需要多少代碼?Python、ruby隻需要一行指令:python -m SimpleHTTPServer 8008或ruby -run -e httpd . -p 8009。對于Golang,需要多幾行代碼(注意是代碼),但是能帶來無與倫比的性能(咱不和Nginx比,這裡也沒說前端代碼的事)。

2.1 編寫代碼

在%GOPATH%/src/Test目錄下建立HelloWeb檔案夾,再在該檔案夾下建立helloweb.go的文本檔案,輸入如下代碼:

package main
import (
    "net/http"
    "io"
)
func sayHello(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "{\"msg\": \"Hello," + r.URL.Query().Get("n") + "!\"}")
}
func main() {
    http.Handle("/", http.FileServer(http.Dir("html")))
    http.HandleFunc("/hello", sayHello)
    http.ListenAndServe(":8001", nil)
}
           

在%GOPATH%/src/Test/HelloWeb/目錄下建立html檔案夾,再在html/檔案夾下建立index.html檔案,寫入一些html代碼或幹脆隻寫:Hello,This is a static html page.

2.2 執行代碼

在cmd中執行:go run helloweb.go

打開浏覽器,在位址欄輸入:http://localhost:8001

Golang學習筆記(二):第一個可用于生産環境下的真正Go程式1. 緣起2. 再來一個Hello World,但這次是個Web站點!3 真正的生産環境站點……4 結語

在位址欄輸入:http://localhost:8001/hello?n=LinBaolong

Golang學習筆記(二):第一個可用于生産環境下的真正Go程式1. 緣起2. 再來一個Hello World,但這次是個Web站點!3 真正的生産環境站點……4 結語

2.3 代碼解析

  • mian()中的第一行代碼啟用了一個靜态站點的處理程式,如果我們在html目錄下放置一個html靜态頁面站點,這就是一個标準的可在生産環境中使用的網站了,性能沒Nginx好,但也可以吊打其他web server了(參考其他人的測試資料,自己沒實測);
  • mian()中的第二行代碼啟用了一個處理程式(API接口程式),處理來自url為/hello的請求,處理程式為sayHello()函數,在sayHello()函數中我們通過r.URL.Query().Get(“n”)來擷取來自Get請求url中的參數n;
  • mian()中的第三行代碼啟動了一個端口為8001的web服務。

3 真正的生産環境站點……

上面弄了個HelloWeb,已經可以做無資料庫支援的帶有簡單互動的網站了,但真正的生産環境站點一般具有以下特點:

  • 真正生産環境站點一般都有配置檔案;
  • 真正生産環境站點一般都要連接配接資料庫;
  • 真正生産環境站點一般都有API(前後端分離的情況),且用Ajax互動,互動資料用json;
  • 真正生産環境站點一般都有錯誤處理和日志;
  • 真正生産環境站點開發時一般由多個go檔案組成。

下面我們就建立一個滿足上面五點要求的站點(架構)。

3.1 項目代碼及詳細注釋

3.1.1 項目目錄結構

demoWeb
│  config.json
│  demoweb.go
│  go.mod
│
├─config
│      config.go
│      go.mod
│
├─database
│      go.mod
│      myDataApi.go
│
├─html
│      index.html
│
├─logger
│      go.mod
│      logger.go
│
└─template
    ├─news
    │      newslist.html
    │
    └─user
            login.html
           

每個子產品的檔案夾下都有一個go.mod檔案,這将在後面3.2.1中解釋。

3.1.2 主程式demoweb.go

在項目根目錄下建立主程式demoweb.go 檔案

package main

import (
	"fmt"
	"net/http"
	"html/template"

	dbApi "demoWeb/myDataApi"
	"demoWeb/logger"
	"demoWeb/config"
)

func main() {
	//測試日志
	logger.Info.Println("程式啟動……")

	//測試配置檔案子產品
	fmt.Printf("測試配置檔案讀取,讀取ServerPort為: %s\n", config.ServerPort())

	//測試資料庫讀取
	fmt.Printf("測試資料庫讀取,系統共有注冊使用者: %d 個\n", dbApi.GetRecordCount("user", "1=1"))
	
	//注冊靜态頁面
	http.Handle("/", http.FileServer(http.Dir("html")))
	
	http.HandleFunc("/hello", sayHello)
	http.HandleFunc("/login", LoginHandler)
	http.HandleFunc("/news", NewsHandler)
	serverErr :=http.ListenAndServe(":"+config.ServerPort(), nil)

	if nil != serverErr {
		logger.Error.Panic(serverErr.Error())
	}
}

func sayHello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Hello,"+r.URL.Query().Get("n")+"!")
}

//Login頁面處理程式
func LoginHandler(w http.ResponseWriter, r *http.Request){
	t, err := template.ParseFiles("template/user/login.html")
	if err != nil {
		logger.Error.Println(err)
	} 
	var nam, pwd, msg string
	nam = r.FormValue("name")
	pwd = r.FormValue("pswd")
	msg = "請輸入使用者名及密碼"
	if nam != "" {
		res :=dbApi.GetRecordCount("user", "name='" + nam + "' and password='" + pwd + "'")
		if  res>0 {
			http.Redirect(w, r, "/news", http.StatusTemporaryRedirect)
		}else if res==0 {
			msg = "使用者名或密碼錯誤!登入失敗!"
		}else{
			msg = "資料庫錯誤!登入失敗!"
		}
	}
	t.Execute(w,map[string]interface{}{"PageTitle": "登入頁面", "Message": msg})
}

//News頁面處理程式
func NewsHandler(w http.ResponseWriter, r *http.Request){
	t, err := template.ParseFiles("template/news/newslist.html")
	if err != nil {
		logger.Error.Println(err)
	} 
	news, err := dbApi.GetRecordData("SELECT title, content FROM `news`")
	if err != nil {
		logger.Error.Println(err)
	} 
	t.Execute(w, news)
}

           

3.1.3 日志及錯誤處理子產品logger.go

在項目根目錄下建立logger檔案夾(Go建議每個包要有獨立的檔案夾),在該檔案夾下建立logger.go檔案(即路徑為./logger/logger.go)

package logger

import (
	"io"
	"log"
	"os"
)
//包中對外公開的全局變量,首字母要大寫
var (
	Info    *log.Logger
	Warn    *log.Logger
	Error   *log.Logger
)
//本包對Go标準庫log包進行了簡單封裝,建立了三種級别的日志
func init() {
	logFile, err := os.OpenFile("demoweb-out.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatalln("打開日志檔案失敗:", err)
	}
	//建立三種的日志,以不同的字首區分
	Info  = log.New(io.MultiWriter(os.Stdout, logFile), "[Info]:",  log.Ldate|log.Ltime|log.Lshortfile)
	Warn  = log.New(io.MultiWriter(os.Stdout, logFile), "[Warn]:",  log.Ldate|log.Ltime|log.Lshortfile)
	Error = log.New(io.MultiWriter(os.Stderr, logFile), "[Error]:", log.Ldate|log.Ltime|log.Lshortfile)
}
           

3.1.4 配置檔案讀取子產品config.go

  • 配置檔案config.json

在項目根目錄下建立配置檔案config.json

{
  "ServerPort": "8001",
  "DatabaseSource": "TestUser:[email protected](127.0.0.1:3306)/test_go?charset=utf8"
}
           
  • 配置檔案讀取子產品config.go

在項目根目錄下建立config檔案夾(Go建議每個包要有獨立的檔案夾),在該檔案夾下建立config.go檔案(即路徑為./config/config.go)

package config

import (
	"encoding/json"
	io "io/ioutil"
	
	"demoWeb/logger"
)

//結構應與config.json檔案一緻
type configs struct {
	ServerPort     string
	DatabaseSource string
}

var configFile string = "config.json"

//若不允許其他子產品修改的全局變量,建議首字母小寫(私有),再設定讀取函數,傳回有關參數值
var conf configs

//go初始化函數,在引用包時就執行,且先于main()執行,且一個項目中多處引用也隻執行一次
//一個包中可以有多個init()函數,一個檔案中也可以有多個init()函數
func init() {
	data, err := io.ReadFile(configFile)
	if err != nil {
		logger.Error.Fatalln("讀取配置檔案失敗!程式終止!")
	} else {
		datajson := []byte(data)
		err = json.Unmarshal(datajson, &conf)
		if err != nil {
			logger.Error.Fatalln("配置檔案已讀取,但解析失敗!程式終止!")
		}
	}
}

//傳回配置參數ServerPort
func ServerPort() string {
	return conf.ServerPort
}

//傳回配置參數DatabaseSource
func DatabaseSource() string {
	return conf.DatabaseSource
}

           

3.1.5 資料庫操作子產品

  • 首先需要安裝mysql驅動子產品

在cmd中執行如下指令:

go get -u github.com/go-sql-driver/mysql
           
  • 資料庫操作子產品myDataApi.go

在項目根目錄下建立database檔案夾(Go建議每個包要有獨立的檔案夾),在該檔案夾下建立myDataApi.go檔案(即路徑為./database/myDataApi.go)。

package myDataApi

import (
	"database/sql"
	"demoWeb/config"
	"demoWeb/logger"

	_ "github.com/go-sql-driver/mysql"
)

var myDB *sql.DB

//功能:初始化資料庫,建立MySQL連接配接池
func init() {
	//使用database/sql包的sql.Open函數建立資料庫對象。它的第一個參數是資料庫驅動名,第二個參數是一個連接配接字串。
	//使用sql.Open函數建立一個連接配接池對象,不是單個連接配接。在open的時候并沒有去連接配接資料庫,隻有在執行query、exce方法的時候才會去實際連接配接資料庫。在一個應用中同樣的庫連接配接隻需要儲存一個sql.Open之後的db對象就可以了,不需要多次open。
	//有全局變量myDB的情況下,err應該單獨聲明,不能用 myDB, err := sql.Open("mysql", config.DatabaseSource()),否則myDB會被處理成局部變量
	var err error
	myDB, err = sql.Open("mysql", config.DatabaseSource())
	if err != nil {
		logger.Error.Fatalf("資料庫連接配接錯誤!%v\n", err)
	}
	//設定打開資料庫的最大連接配接數。包含正在使用的連接配接和連接配接池的連接配接。如果你的函數調用需要申請一個連接配接,并且連接配接池已經沒有了連接配接或者連接配接數達到了最大連接配接數。此時的函數調用将會被block,直到有可用的連接配接才會傳回。設定這個值可以避免并發太高導緻連接配接mysql出現too many connections的錯誤。該函數的預設設定是0,表示無限制。
	myDB.SetMaxOpenConns(2000)
	//用于設定最大閑置的連接配接數。可以避免并發太高導緻連接配接mysql出現too many connections的錯誤。設定閑置的連接配接數則當開啟的一個連接配接使用完成後可以放在池裡等候下一次使用。
	myDB.SetMaxIdleConns(1000)
	//調用了Ping之後,連接配接池會初始化一個資料庫連接配接。
	myDB.Ping()
}

//功能:擷取目前資料庫myDB中tabaleName表内滿足whereCondiction條件的記錄條數
func GetRecordCount(tabaleName string, whereCondiction string) int32 {
	//使用database/sql包的sql.Open函數建立資料庫對象。它的第一個參數是資料庫驅動名,第二個參數是一個連接配接字串。
	//使用sql.Open函數建立一個連接配接池對象,不是單個連接配接。在open的時候并沒有去連接配接資料庫,隻有在執行query、exce方法的時候才會去實際連接配接資料庫。在一個應用中同樣的庫連接配接隻需要儲存一個sql.Open之後的db對象就可以了,不需要多次open。
	//myDB, err := sql.Open("mysql", config.DatabaseSource())
	//調用sql.DB.Query()方法,執行SQL SELECT語句
	row, err := myDB.Query("SELECT COUNT(*) FROM `" + tabaleName + "` WHERE " + whereCondiction)
	if err != nil {
		logger.Error.Printf("Query failed, err:%v", err)
		return -1
	}

	//在使用query時需要管理連接配接,也就是把連接配接釋放,歸還連接配接池,通常采用defer db.Close();而exce在執行完對資料庫的操作後會自動釋放。
	//defer關鍵字是延時調用的意思,row.Close()被放入延遲調用隊列,當目前函數GetRecordCount()結束時才調研row.Close()
	defer row.Close()

	if row == nil {
		logger.Info.Println("no data")
		return 0
	}
	var cnt int32
	for row.Next() {
		//使用row.Scan()擷取查詢到的資料,在此之前必須調用row.Next(),雖然明知隻有一行
		row.Scan(&cnt)
	}
	return cnt
}

//本函數輸入一條SELECT的SQL語句,并将結果資料組裝成[]map[string]interface{}結構傳回;
//函數傳回類型[]map[string]interface{}是map[string]interface{}類型的切片(可認為是動态數組),可存儲多條記錄;
//map[string]interface{}是key-value形式存儲一條記錄,key為字段名(string類型),value為資料值(interface{}任意類型);
//interface{}是為實作多态功能,可以放入任意類型資料;
//想要完整了解[]map[string]interface{}結構,需要先了解Go的切片(Slice)、接口(interface)、映射(map)
func GetRecordData(strSQL string) ([]map[string]interface{}, error) {
	rowData := make([]map[string]interface{}, 0)
	//執行SQL,結果資料集存入rows
	rows, err := myDB.Query(strSQL)
	if err != nil {
		return rowData, err
	}
	//延時調用rows.Close()(再本函數執行結束時調用),釋放資料庫連結
	defer rows.Close()
	//擷取列集合
	columns, err := rows.Columns()
	if err != nil {
		return rowData, err
	}
	cloCount := len(columns)
	//下面代碼需要先了解Go的切片(Slice)、接口(interface)、映射(map)再閱讀
	values := make([]interface{}, cloCount)
	valuePtrs := make([]interface{}, cloCount)
	for rows.Next() {
		for i := 0; i < cloCount; i++ {
			valuePtrs[i] = &values[i]
		}
		rows.Scan(valuePtrs...)
		entry := make(map[string]interface{})
		for i, col := range columns {
			var v interface{}
			val := values[i]
			b, ok := val.([]byte)
			if ok {
				v = string(b)
			} else {
				v = val
			}
			entry[col] = v
		}
		rowData = append(rowData, entry)
	}
	return rowData, nil
}
           

3.1.6 前端Html代碼

  • 首頁——純靜态頁面

    在項目根目錄下建立html檔案夾,在該檔案夾下建立index.html檔案(即路徑為./html/index.html)。

<!DOCTYPE html>
<html lang="zh-ch">
<head>
    <meta charset="utf-8">
    <title>首頁</title>
</head>
<body>
    點選登入系統-><a href="login">登入</a>
</body>
</html>
           
  • 登入頁
<html>
<head>
    <title>{{ .PageTitle }}</title>
</head>
<body>
    <form action="" method="post">
        使用者名:<input type="text" name="name"> 密 碼:<input type="text" name="pswd">
        <input type="submit" value="登入">
    </form>
    <p><b> {{ .Message }}</b></P>
</body>
</html>
           
  • 新聞清單頁

本頁還需要進一步将傳回的記錄處理展示,這裡就不展開了……

<html>
<head>
    <title>新聞清單</title>
</head>
<body>
    <p><b> {{ . }}</b></P>
</body>
</html>
           

3.1.7 MySQL表結構

CREATE TABLE `news` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(45) DEFAULT NULL,
  `content` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `user` (
  `name` varchar(45) NOT NULL,
  `password` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

           

3.2 執行程式

3.2.1 go.mod

每個子產品的檔案夾下都有一個go.mod檔案,從 Go1.11 開始,golang 官方支援了新的依賴管理工具go mod,在相應目錄下使用 go mod init 指令即可在該目錄下生成go.mod檔案。但 Go1.13中對工程中其他子產品的引用又有較大改動,對于非标準庫的子產品,預設的引用路徑是要求以域名開始的,比如:“github.com/go-sql-driver/mysql”,若使用“demoWeb/config”引用目前項目中的包而沒在go.mod中使用“replace demoWeb/config => ./config”重定向的話,運作将會報如下錯誤:

go: demoWeb/[email protected].0.0-00010101000000-000000000000: malformed module path "demoWeb/config": missing dot in first path element
           

是以需要在go.mod中手動添加replace,如以下主目錄下的go.mod:

module demoWeb

replace demoWeb/config => ./config

replace demoWeb/myDataApi => ./database

replace demoWeb/logger => ./logger

go 1.13

require (
	demoWeb/config v0.0.0-00010101000000-000000000000 // indirect
	demoWeb/logger v0.0.0-00010101000000-000000000000 // indirect
	demoWeb/myDataApi v0.0.0-00010101000000-000000000000 // indirect
	github.com/go-sql-driver/mysql v1.5.0 // indirect
)

           

上面的require中的内容,是在go run 指令運作程式後自動生成的。

以下是config目錄下的go.mod,注意“…/logger”是使用相對路徑。

module config

replace demoWeb/logger => ../logger

go 1.13
           

以下是logger目錄下的go.mod:

module logger

go 1.13
           

以下是database目錄下的go.mod:

module myDataApi

replace demoWeb/config => ../config

replace demoWeb/logger => ../logger

go 1.13
           

3.2.2 執行

在cmd中執行go run demoWeb.go(需用cd指令定位到目前工程目錄)

D:\Develop\GoPath\src\demoWeb>go run demoweb.go
go: finding github.com/go-sql-driver/mysql v1.5.0
go: downloading github.com/go-sql-driver/mysql v1.5.0
go: extracting github.com/go-sql-driver/mysql v1.5.0
[Info]:2020/01/13 22:41:11 demoweb.go:15: 程式啟動……
測試配置檔案讀取,讀取ServerPort為: 8001
測試資料庫讀取,系統共有注冊使用者: 1 個

           

打開浏覽器,在位址欄輸入:http://localhost:8001 ,如下:

Golang學習筆記(二):第一個可用于生産環境下的真正Go程式1. 緣起2. 再來一個Hello World,但這次是個Web站點!3 真正的生産環境站點……4 結語

點選【登入】,進入登入頁:

Golang學習筆記(二):第一個可用于生産環境下的真正Go程式1. 緣起2. 再來一個Hello World,但這次是個Web站點!3 真正的生産環境站點……4 結語

随便輸入一個不存在的賬号、密碼,會顯示如下頁面,

Golang學習筆記(二):第一個可用于生産環境下的真正Go程式1. 緣起2. 再來一個Hello World,但這次是個Web站點!3 真正的生産環境站點……4 結語

若是輸入正确的密碼即可跳轉的新聞清單頁面。

4 結語

至此,完成了第一個可用于生産環境下的真正Go程式,當然,還有很多不足,但一個簡單的架構是有了。