安装以及基本语法参考官方文档即可。
入门资源分享:
环境变量
-
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)
}