Golang - 語言基礎 Slice

Golang 入門教學 tutorial

Go 是現代化、速度相當快且擁有豐富的標準庫 透過 goroutine 併發非常有效率,單一線程可以執行多個 goroutine。 在設計併發架構最困難的部分在於如何確保多個併發進程、線程及groutine 不會同時針對同一資料進行操作。透過 Go 可以簡單地實現併發過程數據一致性。 Go 提倡組合(composition),而不是傳統繼承方式;因此,可直接多個類型組合成類型,並且引用這個類型就能使用整個組合的功能。 Go 在內存管理也處理得相當好,使用現代化的回收機制。

常用命令

在建構 go 過程中,常使用到的命令包括:

  • go build : 打包package,會聯同編譯關聯的內容(會忽略 _ 或 . 開頭的go文件)
  • go run : 編譯及運行 go,但不會產生執行檔
  • go clean : 移除打包的 package
  • go fmt : 代碼風格自動格式化
  • go get : 取得遠程代碼並且安裝
  • go test : 自動運行 *_test.go 的文件,並且產生執行檔

其他常用輔助查詢的命令包括:

  • go fix : 修復舊版本的程式碼為新版本
  • go version : 查詢目前版本
  • go env : 查詢環境變數
  • go list : 列出目前安裝的 package
  • go help : 取得幫助

main 與 init

Go 預設有兩個保留函式: init() 與 main()。在 Go 程序執行過程會自動調用這兩者,這兩個函式主要負責執行內容,並且不能返回值。

在接續會提到的 package ,多數都會在最後執行一次 inti(),並且,在主要的程序中,也可以使用 init() 執行動作。

最後,才會執行 main()。

整體而言,在主要程序中,main 主要負責執行初始化程式,大多程式執行入口都由 main() 負責運作。

基本組成 - Package

Go 是由 Package 所組成,其中包括 main package 以及預設執行的 main() 函式。以及每個 package 都會預設初始化一段init()

Go 的 package 載入相當有效率,每一個 package 無論被 import 多少次,整體程序都只會一次載入。

載入 package 的方式如下,可以透過 ```import``

CVT2HUGO: 來載入 package
import {package名稱}

例如

package main

import "fmt"

import "math"

當有多個 package 需要載入,可以用刮號() 來包住

package main
import (
	"fmt"
	"math"
)

import 也可以載入遠端的package 例如:

import "github.com/spf13/viper"

輸出字串

由 Go 一開始的範例都會輸出一段 Hello world

輸出字串的方式是透過 fmt package 的方法來實作

package main
import (
	"fmt"
	"math"
)
func main(){
	fmt.Printf("輸出一行文字但不換行")

	fmt.Println("輸出一段文字並且換行")
}

型別

Go 的型別主要有以下幾種

布林
bool

字串
string

整數
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

位元
byte // uint8 的別名

Unicode碼
rune // int32 的別名

浮點數
float32 float64

複數
complex64 complex128

變數

透過 var

CVT2HUGO: 可以用來定義變數並且可以指定型別
var a int
var b sting
var c bool

可以同時將多組同型別的變數在最後變數進行一次定義

var a, b, c int

此外,也可忽略型別,直接由表達式取得型別

var a, b, c = 1, true, "Hello"

如果在函式內部(注意!只能函式內部)可以使用 := 來取代 var

func main(){
   a := "hi"
   fmt.Println(a)
}

輸出取代字元

fmt.Println 或 fmt.Printf 輸出時,都可以用取代字元

var name = "adam"
var age = 30
fmt.Printf("Your name is %s and age %d\n", name, age)

相關取代字元可以參考 fmt package 說明 https://golang.org/pkg/fmt/

串接字串、數字

透過 fmt.Sprintf

CVT2HUGO: 來串接內容
package main

import "fmt"

type UserData struct {
	name string
	age  int
}

func User(row UserData) string {
	return fmt.Sprintf("Your name is %s and age is %d", row.name, row.age)
}
func main() {
	a := UserData{"adam", 10}
	fmt.Printf(User(a))
}

常數

const 可以用來定義常數

定義常數就不可再更換值

const a = "this is const"
func main(){
	fmt.Println(a)
}

函式

函式是Go的核心設計,主要透過 func 聲明 Go 的函式基本架構如下:

func 函式名稱(){

}

也可以設定帶入參數及定義要返回的型別(可返回多個值)

func 函式名稱(參數 型別, 參數 型別,...) 指定型別{
	return 指定型別值, 指定型別值,...
}

例如

package main

import "fmt"

func addOne(x int) int {
	return x++
}

func main() {
	fmt.Println(addOne(1))
}

如果函式需要帶多個參數,且皆為同樣型別,只需要在最後一個參數定義型別

func add(x, y, z int) int {
	return x + y - z
}

同時,函式可支援多個返回值

並且,如果多個變數要接收這些值,要用 :=

func myfn(x, y sting) (string, string){
	return x, y
}

func main(){
    a, b := myfn("Hello", "Go")
	fmt.Println(a, b)
}

迴圈

透過 for迴圈,基本的構造可以直接參考下方範例

var sum = 0

func main() {
	for i := 0; i < 10; i++ {
		sum += i
	}
	fmt.Println(sum)
}

除此外,可以跟C還有Java一樣,可以忽略前後 (有無分號 ; 都可以)

(其實用法就如同其他語法的 while )

var sum = 0

func main() {
    i := 0
	for i < 10 {
		sum += i
	    i++
	}
	fmt.Println(sum)
}

條件式

If else 與法如下

func checkAnser(a int) string {
	if a > 20 {
		return "Good!"
	} else if a > 10 {
  		return "Good Job!"
	} else {
		return "Go Go Go!"
	}
}

func main() {
	fmt.Println(
		checkAnser(1),
		checkAnser(11),
		checkAnser(21),
	)
}

switch 在 Go 的寫法不必添加 break,只要有匹配到,執行完匹配的內部後就會自動跳脫 (如果不想自動跳脫,可以使用 fallthrough 讓程序繼續往下,但你應該不會這麼做)

func main() {
  sexual := "worman"
  switch sexual {
	case "man":
		fmt.Println("Man")
	case "worman":
		fmt.Println("Worman")
	default:
		fmt.Printf("No selected")
	}
}

defer

Defer 是相當有意思的設計,意思如字面為延遲(defer) 在函示進行 return 時,會將所有定義 defer 的描述進行反方向執行,也就是後進先出的次序 在程式設計過程,會需要在多個地方重複定義一樣的動作 就可以透過 defer 減少程式量

例如,原本需要多次呼叫file.Close()

func myFn() bool {
	file.Open("myfile")
  if checkPoint1 {
  	file.Close()
    return true
  }
  if checkPoint2 {
  	file.Close()
    return false
  }
  file.Close()
  return false
}

可以簡化為

func myFn() bool {
	file.Open("myfile")
  defer file.Close()
  if checkPoint1 {
    return true
  }
  if checkPoint2 {
    return false
  }
  return false
}

結構

結構體格式如下

type Box struct {
	price int
	size string
}

可以直接呼叫,也可直接帶入值

v := Box{}
//or
v := Box{30, "XL"}

範例:

type Box struct {
	price int
	size string
}

func main() {
  	v := Box{}
		v.price = 30
  	v.size = "XL"
	fmt.Println(v.price, v.size)
}

結構可以透過 & 前綴來分配新的共用結構

type Box struct {
	price int
	size  string
}

func main() {
	a := Box{10, "XL"}
	b := &a
	b.price = 6
	b.size = "S"
  	a.size = "M"
	fmt.Println(a.price, a.size, b.price, b.size)
}

輸出結果為 ```6

函式與結構

一般函式可以直接帶入結構的方式來應用

CVT2HUGO: M 6 M```

type Vertex struct {
	X, Y float64
}

func Abs(v Vertex) float64 {
	return v.X+v.Y
}

func main() {
	a := Vertex{3, 4}
	fmt.Println(Abs(a))
}

也可以透過結構的方式來定義函式,例如:

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return v.X+v.Y
}

func main() {
	a := Vertex{3, 4}
	fmt.Println(a.Abs())
}

陣列

在 Go 的陣列結構為 [n]T

  • n 是數字,用來定義陣列能存放值的數量
  • T 是型別,定義存放值得類型

定義的長度 n 後,所存放的直必須要符合這個數量(後面會提到 slice 能以更有彈性的方式定義 array)

	var a [2]int
	a[0] = 1
	a[1] = 2
  fmt.Println(a)

slice 另外,可以透過 slice 方式來定義,不必聲明值得數量,直接在 T 以 slice 方式來聲明

[]T

並且,在這樣的定義方式,會包含了這個陣列長度的訊息,可以透過 len() 來取得長度 例如:

	p := []int{1,2,3,5,6,7}
	for i := 0; i < len(p); i++ {
		fmt.Printf("Hello %d is %d\n", i, p[i])
	}

slice 聲明後,可以對陣列進行分割

p[1:2]   表示從陣列 1  2-1
p[:2]    表示從陣列 0  2-1
p[1:]    表示從陣列 1 到最後一個元素

pre, len 與 cap 一個 slice 被建立時,本身會帶有以下幾個特徵 pre 代表 slice 的起始指標 len 可以取得 slice 陣列的,如果被分割,則取得的會是分割後的長度 cap 可以取得 slice 容量,只要起始指標維持在一開始,即使被分割,取得的仍是最一開始定義的量,若 pre 變動才會影響 cap 值 例如:

		p := []int{0,1,2,3,4,5}
  	a := p[1:3] # 透過切割方式此此時在 pre 起始指標會被移動到 1 
  	fmt.Println("len = ", len(a))		//以 [1:3] 的長度為 2 個
  	fmt.Println("cap = ", cap(a))   //由於 pre 指標已經移動到 1,剩下的內容為 5

make make 可以用來創建一個 slice ,並且可定義 len, cap 值 例如 make([]int, 如果是 make([]int, CVT2HUGO: 5, 10) 可以建立一個 len=5, cap=10 的 slice CVT2HUGO: 5) 則會建立一個 len=5, cap=5 的 slice

map Map 可以用來定義 key 以及對應 value

  person := map[string]int{
		"number": 1,
		"age": 18,
		"height": 183,
	}

並且可以直接透過 key 取出值

person["number"] 
person["age"] 
person["height"] 

其中, value 可以搭配 struct 來增加儲存內容的彈性,例如:

type Vertex struct {
	string
	int
	float64
}

var m = map[string]Vertex{
	"Tom":   {"hi", 40, 22.32},
	"Merry": {"Hello", 37, 23.21},
}
fmt.Println(m)
//輸出 map[Merry:{Hello 37 23.21} Tom:{hi 40 22.32}]

range range 可以將 slice, map 進行迭代,在 for 循環可以透過這方式取出 key, value ,例如:

	a := []int{1, 2, 3, 4, 5, 6, 7}
	for key, value := range a {
		fmt.Println("Key:", key, "Value:", value)
	}
/*
輸出
Key: 0 Value: 1
Key: 1 Value: 2
Key: 2 Value: 3
Key: 3 Value: 4
Key: 4 Value: 5
*/

搭配 range 來取出 map 中的 key, value:

  	person := map[string]int{
		"number": 1,
		"age": 18,
		"height": 183,
	}
	for key, value := range person {
		fmt.Println("Key:", key, "Value:", value)
	}

檢查 map 的 Key 是否存在

再取出資料過程,時常會需要檢查是否存在,在判斷是否接續其他動作 判斷 key 是否存在的方式如下, elem 是變數,第二的 ok 會檢查 map 的 key 是否存在

elem, ok = m[key]

這裡示範先透過delete刪除 map 中的內容,在透過檢查方式來判斷

	m := make(map[string]int)

	m["age"] = 1234
  
  	//檢測元素
	value, checkIsset := m["age"]

	fmt.Println(value, checkIsset) //1234 true
  
	//Delete element with key = "age"
	delete(m, "age")

  	//檢測元素
	value, checkIsset = m["age"]

	fmt.Println(value, checkIsset) //0 false

Interface

Interface 是 Go 最精華的部分,可以讓物件導向更容易實現。

在 Golang 的 Interface 主要負責把 method 集合起來,統整成一個接口。 例如,定義一個 Interface 將資料庫操作的方法集合起來,之後可以用這個 Interface 來作為 ORM,用統一格式規範出 MySQL, Mongo, Redis…等資料庫連線。

更多內容將於後續介紹

Go Interface

Go 語言最精華的部分就是 Interface,可以透過 Interface 來定義規範,將 method 組合成統一的接口,讓系統本身可以經由 Interface 來獲得更多的拓展。

底下為簡單範例

package main

import "fmt"

type Members interface {
	GetName() string
}

type P1 struct {
	name string
}

func (r *P1) GetName() string {
	return r.name
}

func Output(m Members) {
	fmt.Printf("Your name is %s", m.GetName())
}

func main() {
	a := &P1{
		name: "Adam"
	}
	Output(a)
}