安裝以及基本語法參考官方文檔即可。
入門資源分享:
環境變量
-
GOPATH 是什么?
GOPATH設置目錄用來存放Go源碼,包管理路徑,Go的可運行文件,以及相應的編譯之后的包文件。
$GOPATH
下通常會存在三個文件夾:src(存放源代碼),pkg(包編譯后生成的文件),bin(編譯生成的可執行文件)。
$GOPATH
應該只有一個路徑,每個go項目都應當做一個包,并且存放于src目錄下。 -
GOROOT是什么?
GOROOT是Golang的安裝環境,全局唯一的變量,通常不用修改。
語法學習
數組與切片slice
Go中數組是值類型。aa = array 時會拷貝array中所有元素到aa。
對于聲明了長度的數組,其長度不可再更改。
a:=[3]{1,2,3}
a[3]=4//Error: invalid array index 3 (out of bounds for 3-element array)
slice 可以看做是容量可變的數組。初始化時可以為其指定容量,但是其空間大小會隨著內容長度的變化進行再分配。切片是引用類型。
func main() {
s:=make([]int,0)
// s:=[]int{}
fmt.Println(s)
println(s)
s=append(s,1)
fmt.Println(s)
println(s)
}
// ouputs are
[]
[0/0]0x1155c08
[1/1]0xc4200180b8 //地址已經變化
[1]
聲明并賦值一個一維數組
arr := [3]int{1,2,3}
var arr [3]int = [3]int{1, 2, 3}
定義一個空數組
var arr []int
對數組賦值:可以在其長度內直接賦值。
```
var arr [2]int
arr[0]=1
//arr is [1, 0]
```
對slice賦值,不能arr[0]=1,會報錯“index out of range”。可以通過append方法實現。
```
var a []int
a = append(a, 1)
// a=[1]
```
for 循環的使用方式
-
最常見使用方式:
for i := 0; i < count; i++ { }
-
當做while 使用:
for condition { }
-
for range
for index,item := range arr { }
注意返回值 return
如果函數有返回值,必須在函數聲明時指定其返回類型
func increase(a int) int{
return a+1
}
在使用別的語言時,可能在函數體中會使用if (conditon) {return }
來停止執行函數體中的內容。但是在Golang 中一個函數只能返回函數定義時指定的類型,所以這種判斷條件要盡可能前置,如果不滿足條件,就不讓其執行該函數。
結構體與指針
結構體可以有自己的屬性和方法。
type Person struct {
name string
age int
}
func (p Person) say() {
fmt.Println("i can say ...")
}
結構體內的屬性簡單易懂,而屬于他的內部方法是通過在定義func時指明其參數是哪個結構類型來實現的。
結構體只是其他類型的組合而已,一樣是值傳遞,所以每次賦值給其他變量都是值拷貝。如果想一直基于一個結構體進行操作,可以使用指針類型的參數。
例子1:實現一個有固定長度的棧,并含有pop/push方法。
package main
import (
"fmt"
)
func main() {
var s stack
s.push(1)
s.push(2)
fmt.Println(s)
fmt.Println(s.pop())
}
type stack struct {
index int
data [10]int
}
func (s *stack) push(v int) {
s.data[s.index] = v
s.index++
}
func (s *stack) pop() int {
s.index--
return s.data[s.index]
}
如果在pop push 方法不是提供指向stack的指針,而是stack類型的變量的話,每次執行push或pop都是基于s的副本進行操作的,所以打印s仍然維全0。
例子2: 嘗試聲明一個底層類型為int的類型,并實現某個調用方法該值就遞增100。如 a=0, a.increase() == 100
。
分析:要實現該值的自增,必須使用指針類型。
package main
import "fmt"
type TZ int
func (z *TZ) increase(){
*z+=100
}
func main() {
a:=TZ(0)
//var a TZ
a.increase()
fmt.Println(a)
}
注意創建TZ類型的 實例時,因為其底層是int類型,所以初始化是a:=TZ(0)
,而不能是a:=TZ{}
。
進一步的,如果increase 方法接受一個被加數。
func (z *TZ) increase(num int){
*z+=num // mismatched types TZ and int
}
可以通過以下方式修改
-
*z+=TZ(num )
進行類型轉換 -
func (z *TZ) increase(num TZ)
函數聲明時直接指定接受參數類型為TZ類型,因為其底層也是int 類型,所以調用increase(10)也完全沒有問題。
接口
接口也是一種獨特的類型,可以看做是一組method的組合,如果某個對象實現了該接口的所有方法,則該對象就實現了該接口(該類型的值就可以作為參數傳遞給任何將該接口作為接受參數的方法)例子如下:兩個struct 類型都實現了接口I 的方法,所以以I作為輸入參數的方法,也都可以接受這兩個struct 類型作為參數。即同一個接口可以被多種類型實現。
package main
import "fmt"
type I interface {
getName() string
}
type T struct {
name string
}
func (t T) getName() string {
return t.name
}
type Another struct {
name string
}
func (a Another) getName() string {
return a.name
}
func Hello(i I) {
fmt.Printf("my name is %s", i.getName())
}
func main() {
Hello(T{name: "free"})
fmt.Println("\n")
Hello(Another{name: "another"})
}
空接口: 定義的一個接口類型,如果沒有任何方法就是一個空接口。空接口自動被任何類型實現,所以任何類型都可以賦值給這種空接口。下邊的例子可以看到,聲明一個空接口類型的變量之后,可以給其賦值各種類型(int,string, struct)而不會報錯。所以利用空接口配合switch type
可以實現泛型。
補充: Golang中通過.(type)
實現類型斷言。
if t, ok := i.(*S); ok {
fmt.Println("s implements I", t)
}
如果ok為真,則i是*S類型的值。
example1:
package main
import "fmt"
type I interface {}
type T struct {
name string
}
func main() {
var val I
val=5
fmt.Printf("val is %v \n",val)
val="string"
fmt.Printf("val is %v \n",val)
val=T{"struct_name"}
fmt.Printf("val is %v \n",val)
switch t:=val.(type) {
case int:
fmt.Printf("type int %T \n",t)
case string:
fmt.Printf("Type string %T \n", t)
case T:
fmt.Printf("Type string %T \n", t)
default:
fmt.Printf("Unexpected type %T", t)
}
}
輸出為:
val is 5
val is string
val is {struct_name}
Type string main.T
example2: 實現一個包含不同類型元素的數組。通過定義一個空接口,然后定義一個接收空接口類型元素的數組即可。
type Element interface{}
type Vector struct{
a []Element
}
實現 Set(index. element)
方法
func (p *Vector) Set(index int, e Element) {
p.a[i]=e
}
協程
Part1: 協程、線程與進程分別是什么?
每個運行在機器上的程序都是一個進程,進程是運行在自己內存空間的獨立執行體。
-
一個進程內可能會存在多個線程,這些線程都是共享一個內存地址的執行體。幾乎所有的程序都是多線程的,能夠保證同時服務多個請求。
但是多線程的應用難以做到精確,因為他們會共享內存中的數據,并以無法預知的方式對數據進行操作。所有不要使用全局變量或共享內存,他們會給你的代碼在并發時帶來危險。解決之道在于同步不同的線程,對數據加鎖,這樣同時就只有一個線程可以變更數據。
因為對多線程加鎖會帶來更高的復雜度,Golang采用協程(goroutines)應對程序的并發處理。協程與線程之間沒有一對一的關系,協程是利用協程調度器根據一個或多個線程的可用性,映射在他們之上的。協程比線程更輕量,使用更少的內存和資源。協程可以運行在多個線程之間,也可以運行在線程之內。
協程的棧會根據需要進行伸縮,不出現棧溢出。協程結束的時候回靜默退出,棧自動釋放。
協程工作在相同的地址空間,共享內存,所以該部分必須是同步的。Go使用channels來同步協程,通過通道來通信
Part2: Go協程的使用
Golang使用通道channel實現協程之間的通信,同一時間只有一個協程可以訪問數據,避開了共享內存帶來的坑。
channel 是引用類型。channel中的通信是必須寫入一個讀取一個的,如果沒有了讀取,也不會再寫入,此時會認為通道已經被阻塞,因為channel中允許只能同時存在一個元素。如果不再寫入,讀取channel也會隨著channel變空而結束。
-
創建
先聲明一個字符串通道,然后創建
var ch1 chan string ch1 = make(chan string)
或者直接創建:
ch1 := make(chan string) //構建int通道 chanOfChans := make(chan int)
-
符號通信符
使用箭頭,方向即代表數據流向
ch <- int1
將int1寫入到channelint2 := <- ch
從channel中取出值賦給int2 -
例子
- 使用
go
關鍵字開啟一個協程 - 兩個協程之間的通信,需要給同一個通道作為參數。
package main import ( "fmt" "time" ) func main() { ch:=make(chan string) go sendData(ch) go getData(ch) time.Sleep(1e9) } func getData(ch chan string){ for{ fmt.Printf("output is %s \n",<-ch) } } func sendData(ch chan string){ ch<-"hello" ch<-"world" ch<-"haha" }
- 使用
一些需要注意的點
變量大小寫
Golang中沒有定義某變量維私有或者全局的關鍵字,而是通過符號名字的首字母是否大小寫來定義其作用域的。這些符號包括變量、struct,interface 和func 。針對func/變量我們應該都已經知道只有首字母大寫才能在別的包中調用該方法/變量,但是定義結構體時我們卻很容易忽略這一點。
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
type Page struct {
title string
filename string
Content string
}
type Pages []Page
var pages = Pages{
Page{
"First Page",
"page1.txt",
"This is the 1st Page.",
},
Page{
"Second Page",
"page2.txt",
"The 2nd Page is this.",
},
}
func main() {
fmt.Println(pages)
pagesJson, err := json.Marshal(pages)
if err != nil {
log.Fatal("Cannot encode to JSON ", err)
}
fmt.Fprintf(os.Stdout, "%s", pagesJson)
}
//output is
[{First Page page1.txt This is the 1st Page.} {Second Page page2.txt The 2nd Page is this.}]
[{"Content":"This is the 1st Page."},{"Content":"The 2nd Page is this."}]
可以見到,經過序列化之后有些數據丟失,并且丟失的全是Page結構體中以小寫字母開頭的變量。結構體其實就是多個變量的聚合,其賦值仍然是值的拷貝,所以在定義結構體時,一定要慎重,一旦后邊要通過其他方法對結構體進行處理,那么就最好要將其首字母大寫(這是避免編譯錯誤的最簡單方法),雖然這種格式=可能對Golang的初學者有點難以接受。
局部變量初始化
在一個func內部,我們可以通過a := anotherFunc(xxx)
來直接對一個局部變量聲明并且初始化,但是要注意每次使用:=
符號時,都是在定義一個新的局部變量。
在進行web開發時,aa,err:= exampleFunc(xxx)
的表達式很常見,注意兩點:
- 等號左邊要至少有一個是未聲明過的變量,否則編譯失敗;
- 要防止等號左邊的局部變量遮蓋了你想要使用的全局變量。
case1 :
func example(){
aa,err := funcA('xxx')
if err != nil {
fmt.Println(err)
return
}
err := funcB('yyyy')
//should be err = funcB('yyyy')
if err != nil {
fmt.Println(err)
return
}
}
這兩個err其實是一個變量,在接受funcB的返回值時的再次聲明會導致編譯錯誤
case2: 在實現增刪改查方法的封裝時,我們一般都會對數據庫進行操作。而在此之前,必須通過"database/sql"包提供的接口func Open(driverName, dataSourceName string) (*DB, error)
實現對數據庫的驗證。
如果是按照如下方式執行Open()方法,那么在routerHandler方法中要如何對同一個db進行操作?
package main
import (
"net/http"
"strings"
"database/sql"
"encoding/json"
_ "github.com/go-sql-driver/mysql"
"fmt"
)
main(){
db, err := sql.Open("mysql", "pass:password@/database_name")
//聲明并初始化了一個DB的指針db.
db.SetMaxIdleConns(20)
http.HandleFunc("/list", getList)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
所以最好先初始化一個全局的指針var DB *sql.DB
。然后在main函數中在對其賦值
var err error
DB, err = sql.Open("mysql", "pass:password@/database_name")
不要使用:=
,否則又將sql.DB的指針賦值給了一個新的變量。也不要妄想我現在已經賦值給了首字母大寫的DB,其他方法中使用該符號不就實現了對同一個數據庫的操作了么?No, :=
定義的是局部變量。
fmt中各種print方法
-
fmt.Printf()
格式化字符串var v int = 12 fmt.Printf("result is %v", v) // result is 12
-
fmt.Println(args)
格式argsfmt.Println("result is %v", v) //result is %v 12,不會打印出格式化字符串
fmt.Fprintf(w,"result is %v", value)
可以將格式化的字符串結果賦值給w。
定義數據格式
場景: web開發中getUsers 接口需要返回json格式的用戶信息。總體思路:先根據返回數據的結構定義一個包含對象的數組接受Mysql的返回值,然后再對該對象序列化。
1.定義User字段
type User struct {
Id int
Username string
Age string
}
2.定義Users字段
type Users []User
3.將從mysql的返回值賦給定義的數組。
func getList(w http.ResponseWriter, r *http.Request){
user := User{}
users := Users{}
rows, err := DB.Query("SELECT * FROM userinfo")
checkErr(err)
for rows.Next() {
err := rows.Scan(&user.Id, &user.Username, &user.Age)
checkErr(err)
users = append(users, user)
}
res, err := json.Marshal(users)
checkErr(err)
w.Write(res)
}