go 语言入门



IDE

GoLand 收费
LiteIDE 免费,用户体验一般

入口函数

可执行程序必须包含 main 函数

如果定义了 main 或 init 函数,启动后会自动执行,不需要显示调用,init 会在 main 之前执行,不管哪个先定义

main/init 外的代码会先执行,不管哪块代码先写

func main() {
    fmt.Println("Hello, World!")
}

func init() {
    fmt.Println("\ninit\n")
}

var b = test()

会先执行 test 再执行 init 再执行 main

变量

var a string
var b string = "abc" // 声明同时初始化
var c = "abc"        // 直接初始化可以省略类型

d := "abc"           // 同时省略 var 和类型,如果 d 已经定义的话,会报错

var a, b, c string   // 可以同时定义、赋值多个

var (                // 定义多个类型不同的变量
    a int
    b bool
)

和其他语言不同,go 的局部变量如果定义但是没使用,也会报错

declared and not used

需要使用变量才行

常量, 枚举, iota

const LENGTH int = 10

常量当作枚举用

const (
    Unknown = 0
    Female = 1
    Male = 2
)

iota 对 const 计数

    const (
        a = iota   // 0
        b          // 1
        c          // 2
        d = "ha"   // "ha"   iota = 3
        e          // "ha"   iota = 4
        f = 100    // 100    iota = 5
        g          // 100    iota = 6
        h = iota   // 7
        i          // 8
    )

遇到新的 const 时 iota 会重置为 0

占位符, nil

var a, _ = test()

空用 nil 表示

函数体外语句

syntax error: non-declaration statement outside function body

错误原因: 函数体外的每个语句,都必须是 golang 的关键字开始

比如不能是

a := 1

test()

必须用

var a = 123
var b = test()

这样才不报错

if、switch、type switch、select、for

go 的 if 条件语句不需要括号

        if result > 100 {
            fmt.Printf("%d + %d = %d\n", a, b, result)
        } else {
            fmt.Println(result)
        }

switch 语法

switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}

type switch 可判断变量类型

  var x = test()

  switch x.(type) {
      case nil:
         ...
      case int:
         ...
      case func(int) float64:
         ...
      case bool, string:    // bool 或 string 类型
         ...
      default:
         ...
  }

select 的所有 case 都必须是通信操作
如果有多个 case 可以执行,随机选择一个
如果都不可执行,那么阻塞直到某个通信可以运行

   var c1, c2, c3 chan int
   var i1, i2 int

   select {
       case i1 = <-c1:
           fmt.Printf("received ", i1, " from c1\n")
       case c2 <- i2:
           fmt.Printf("sent ", i2, " to c2\n")
       case i3, ok := (<-c3):
           if ok {
               fmt.Printf("received ", i3, " from c3\n")
           } else {
               fmt.Printf("c3 is closed\n")
           }
       default:
           fmt.Printf("no communication\n")
   }

go 没有 while 语句,只用 for

for init; condition; post {}

for condition { }

for { }

init; condition; post 这些条件可以是空的

continue, break, goto

   for a < 10 {
      if a == 5 {
         continue;
      }
      a++;
   }

continue 可以加 label

    re:
        for i := 1; i <= 10; i++ {
            for j := i; j <= 100; j++ {
                if i + j == 100 {
                    continue re
                }
            }
        }

break 和 goto 的用法类似,也可以加 label

指针

var a int = 20
var i *int

i = &a
fmt.Printf("%d\n", *i)

和 c 语言一样

数组、切片、range、map

var n [10]int

for i = 0; i < 10; i++ {
    n[i] = i + 100
}

切片就是长度可变的数组

var n []int
var n []int = make([]int, 10)  // 初始化长度

s := arr[startIndex:endIndex]
s := arr[startIndex:]
s := arr[:endIndex]

len(n)

range 可用于迭代

nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
    sum += num
}

集合

var m map[string]string      // 声明,默认是 nil
m = make(map[string]string)  // 初始化

m["key1"] = "value1"
m["key2"] = "value2"

value, ok := m ["key3"]
if ok {
    // 存在
} else {
    // 不存在
}

// 删除
delete(m, "kk")

// 迭代
for k := range m {
    fmt.Println(m[k])
}

注意要用 make 初始化,不然变量是 nil

日期格式化

// 这个 layout 是固定的,必须用这几个数字,不然会报错,这应该是 golang 诞生的时间...
layout := "2006-01-02 15:04:05"

// 按 layout 指定的格式转换成 string
fmt.Printf("current time is %s\n", time.Now().Format(layout))

// t, err := time.Parse(layout, "2021-08-09 12:34:27")
var t time.Time
var err error
t, err = time.Parse(layout, "2021-08-09 12:34:27")   // 按 layout 指定的格式转换成 Time 类型
if err == nil {
    fmt.Println(t.Second())
}

做法和其他语言用 YYYYMMDD 这样的做法不一样,比较直观

struct (类)

go 没有类,继承等概念,而是通过 struct 实现面向对象

type student struct {
    name string
    age  int
}

func (s student) getName() string {
    return s.name
}

func (s * student) setName(name string) {
    s.name = name
}

func (this student) getAge() int {
    return this.age
}

func (this * student) setAge(age int) {
    this.age = age
}

func (this student) display() {
    fmt.Printf("name is %s, age is %d\n", this.name, this.age)
}

可以看到数据和方法的定义是分开的,
方法是在函数定义中加入 struct 比如 (s student),或 (s * student) 表示传引用

初始化和使用

s0 := student{"Li", 20}

s0.display()


s1 := student{}
s1.name = "Wang"
s1.age = 35

s1.display()


s2 := new(student)
s2.name = "Zhang"
s2.age = 30

s2.display()


s3 := new(student)
s3.setName("Mr." + s1.getName())
s3.setAge(s2.getAge() - 5)

s3.display()


s4 := student{name: "Han", age: 12}

s4.display()

和其他语言比还是很不一样

struct tag、reflect 反射、json 序列化

import (
    "fmt"
    "reflect"
)

/*
 * tag 用 `` 标识
 * 由多个 key-value 组成
 * key 和 value 之间用 : 隔开,不能有空格
 * value 用 "" 标识
 * 多个 key-value 之间用空格隔开
 */
type Student struct {
    Name string `json:"name" id:"100"`
    Age  int    `json:"age" id:"101"`
}

func main() {
    student := Student{"Wang", 18}

    // 通过反射拿到类型
    reflectType := reflect.TypeOf(student)
    fmt.Printf("type of student is %+v\n", reflectType)
    ageField, _ := reflectType.FieldByName("Age")
    fmt.Println(ageField.Index)
    fmt.Println(ageField.Name)
    fmt.Println(ageField.Type)

    // 通过反射拿到 tag
    fmt.Println(ageField.Tag.Get("json"))
    fmt.Println(ageField.Tag.Get("id"))

    // 通过反射拿到值
    reflectValue := reflect.ValueOf(student)
    fmt.Printf("value of student is %+v\n", reflectValue)
    nameField := reflectValue.FieldByName("Name")
    fmt.Println(nameField)
}

结果

type of student is main.Student
[1]
Age
int
age
101
value of student is {Name:Wang Age:18}
Wang

比如 json 就用了 tag 实现序列化时使用和变量名不一样的字段名

import (
    "fmt"
    "encoding/json"
)

type Student struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    student := Student{"Wang", 18}

    jsonStr, _ := json.Marshal(student)
    fmt.Println(string(jsonStr))
}

输出 {"name":"Wang","age":18}

通过匿名变量实现 struct 的继承、自定义 String() 方法

type Person struct {
    Name string
    Age int
}

type Teacher struct {
    Title string
    Class int
    Grade int

    Person   // 匿名, 相当于定义了 Name 和 Age 变量,如果 Teacher 本身定义了这两个变量,那挑先定义的
}

func main() {
    teacher := Teacher{}
    teacher.Name = "Li"       // Teacher 没有直接定义 Name 和 Age
    teacher.Age = 30
    teacher.Title = "senior"
    teacher.Class = 3
    teacher.Grade = 6

    fmt.Println(teacher)
}

结果是 {senior 3 6 {Li 30}}

可以看到 {Li 30} 被作为一个子变量,可以自定义 String 方法输出

import (
    "fmt"
    strconv
)

func (this Teacher) String() string {
    return "{name:" + this.Name + ", age:" + strconv.Itoa(this.Age) +
        ", title:" + this.Title + ", class:" + strconv.Itoa(this.Class) +
        ", grade:" + strconv.Itoa(this.Grade) + "}"
}

结果是 {name:Li, age:30, title:senior, class:3, grade:6}

type 关键字

除了定义 struct,还用于类型别名,和函数别名

func main() {
    type myType int
    var m myType = 123
    fmt.Println(m)

    type myFuncType func(int) string
    var fn myFuncType = myFunc
    fmt.Println(fn(5))
}

func myFunc(n int) string {
    return fmt.Sprintf("receive %d", n)
}

其实定义 struct 也相当于是别名

函数的引用传递

func swap(x *int, y *int) {
    var temp int
    temp = *x
    *x = *y
    *y = temp
}

var a int = 100
var b int= 200

swap(&a, &b)

struct 是值传递,如果要传 struct 的引用,同样要用指针

func updateStudent(s *student) {
    s.age *= 2
}

updateStudent(&s0)

数组默认是值传递

func update(array [3]int)  {
    array[0] = 100
}

var array = [3]int{1, 2, 3}
update(array)   // array 的值不会改变

数组的引用传递要用指针

func update(array *[3]int)  {
    (*array)[0] = 100
}

var array = [3]int{1, 2, 3}
update(&array)   // array 的值会改变

切片默认就是引用传递

func update(slice []int)  {
    slice[0] = 100
}

slice := make([]int, 0)
slice = append(slice, 1, 2, 3)
update(slice)   // slice 的值会改变

map 默认引用传递

func update(mapVar map[string]int)  {
    mapVar["key"] = 100
}

mapVar := make(map[string]int)
mapVar["key"] = 1
update(mapVar)   // mapVar 的值会改变

go 的引用传递和 C 差不多

函数闭包

一个外部函数,如果定义了内部函数,并且内部函数引用了外部函数的变量,并且外部函数返回的是内部函数的引用,这个被返回的内部函数,就是闭包

python 的例子

def outer(a):
    base = a

    def inner(n):
        return base ** n

    return inner

f = outer(2)    ## 这个 f 就是闭包
print(f(10))
print(f(3))

go 语言

func outer(a int) func(n int) float64 {
    base := a
    return func(n int) float64 {
        return math.Pow(float64(base), float64(n))
    }
}

f := outer(2)          // 这个 f 就是闭包
fmt.Println(f(10))
fmt.Println(f(3))

通过把函数当作返回类型实现

标识符大小写

标识符(包括常量、变量、类型、函数名、结构字段等等)

大写字母开头的,可以被外部包的代码所使用

小写字母开头的,对包外是不可见的,但包内可见

package

文件的第一行声明该文件所属的包

package fmt

通过 import 引用包

系统自带的 package 在 GOROOT/src

比如

import (
    "fmt"
    "net/http"
)

可以找到目录 GOROOT/src 有

src/
  |- fmt
  |- net
      |- http

同一级目录下的所有文件必须属于同一个包,子目录下的所有文件属于另一个包

包名通常都和目录名一样,并且用小写

自定义 package

src/
  |- test.go
  |- calc
      |- calc.go
          |- func.go

calc.go

package calc

import "errors"

func Calc(num1, num2 int, operator string) (int, error) {
    switch operator {
    case "+":
        return sum(num1, num2), nil
    case "-":
        return minus(num1, num2), nil
    default:
        return 0, errors.New("invalid operator!")
    }
}

func.go

package calc

func sum(num1, num2 int) int {
    return num1 + num2
}

func minus(num1, num2 int) int {
    return num1 - num2
}

test.go

package main

import (
    "fmt"
    "calc"
)

func main() {
    var result, _ = calc.Calc(100, 23, "+")
    fmt.Println(result)
}

可以看到,calc.go 的 Calc 函数可以直接调用 func.go 的 sum 和 minus 函数,因为都是 package calc,都是同一个包的

但是 test.go 必须 import calc,并且只能调 calc.Calc 不能调用 calc.sum,因为它们是不同 package 的

大写开头的标识符可以被包外引用,小写开头的标识符只能被包内引用


但是直接运行会报错

package calc is not in GOROOT (C:\Program Files\Go\src\calc)

解决方案一

go env -w GO111MODULE=off

go env -w GOPATH=xxx  (xxx 就是源代码 src 的上级目录)

解决方案二

go mod init calculator    // 任意名字,在 src 目录下,会产生 go.mod 文件

import 改成

import (
    "fmt"
    "calculator/calc"
)

这样就能正常运行了

引用的 package 里面定义 init() 会被执行

接口

// 定义接口
type Shape interface {
	display()
	reset() 
}

// 定义结构体
type Rect struct {
	m float64
	n float64
}

type Circle struct {
	radis float64
}

// 实现接口 (值传递)
func (this Rect) display() {
	fmt.Printf("this is a rect : %f\n", (this.m*2 + this.n*2))
}

func (this Rect) reset() {
	this.m = 0
	this.n = 0
}

// 实现接口 (引用传递)
func (this *Circle) display() {
	fmt.Printf("this is a circle : %f\n", (2 * math.Pi * this.radis))
}

func (this *Circle) reset() {
	this.radis = 0
}


func main() {
    // 接口对象
    var shape Shape

    // 初始化为实现该接口的结构体,并调用接口
    shape = Rect{5, 10}
    shape.display()
    shape.reset()
    shape.display()  // 因为是值传递,所以实际上 reset 没改变 shape 的值,display 结果没变

    // 初始化为实现该接口的另一结构体,并调用接口
    shape = &Circle{5}    // Circle 实现 Shape 接口用的是引用传递,所以初始化要用上 & 符号
    shape.display()
    shape.reset()    
    shape.display()  // 因为是引用传递,所以 reset 改变了 shape 的值,display 的结果改变了
}

这部分有点像 Java

错误处理

go 语言没有 try...catch... 命令

而是主张把参数作为返回值,由调用者决定怎么处理

import (
	"errors"
	"fmt"
)

func divide(a int, b int) (int, error) {
	if b == 0 {
		return -1, errors.New("除数不能为0")
	}

	return a / b, nil
}

func main() {
	var result, err = divide(6, 0)
	if err != nil {
		fmt.Println(err.Error())
		return
	}

	fmt.Println(result)
}

errors.New("除数不能为0") 返回一个实现了 Error() 接口的结构体 errorString

package errors

func New(text string) error {
	return &errorString{text}   // errorString 通过引用传递实现 error 接口,所以初始化要加上 & 符号
}

type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

error 是一个接口

type error interface {
    Error() string
}

errors.New("除数不能为0") 不能格式化字符串,可以改成

fmt.Errorf("除数不能为 %d", 0)

可以参考 errorString 按自己的需求,自定义自己的 struct

defer (延迟执行)

defer 语句会被延迟处理,在 defer 所属的函数即将返回时,会将 defer 语句返序执行

func main() {
	fmt.Println("start")
	defer fmt.Println("defer 1")
	defer fmt.Println("defer 2")
	fmt.Println("end")

结果是

start
end
defer 2
defer 1

可用于释放资源,比如关闭文件,释放锁等等

f1, err1 := os.Open(filename1)
defer f1.Close()
f2, err2 := os.Open(filename2)
defer f2.Close()

// 执行文件操作

甚至程序奔溃后还会执行

可以简化代码,作用就类似于 java 的 final

panic (崩溃) 和 recover (恢复)

作用类似于 throw

func func1() {
	panic("unknown error occur")
	fmt.Println("func1")
}

func func2() {
	func1()
	fmt.Println("func2")
}

func main() {
	fmt.Println("start....")
	func2()
	fmt.Println("end")
}

结果为

start....
panic: unknown error occur

goroutine 1 [running]:
main.func1(...)
	xxx/test.go:125
main.func2()
	xxx/test.go:130 +0x27
main.main()
	xxx/test.go:12 +0x8a
exit status 2

可以看到后面的 end 不会被执行

如果希望程序能继续执行,可以用 recover,类似于 catch,并且 recover 必须写在 defer 函数里面

func func1() {
	panic("unknown error occur")
	fmt.Println("func1")
}

func func2() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("panic: %v\n", err)
		}
	}()

	func1()
	fmt.Println("func2")
}

func main() {
	fmt.Println("start....")
	func2()
	fmt.Println("end")
}

结果

start....
panic: unknown error occur
end

panic 语句被 func2 的 defer 函数里的 recover 捕获,这样 func2() 后面的代码能继续执行

模块管理

如下面导入三方包

import (
    "github.com/petermattis/goid"
)

可能会报错

test.go:8:2: no required module provides package github.com/petermattis/goid; to add it:
    go get github.com/petermattis/goid

需要执行命令按照包

go get github.com/petermattis/goid

如果报错可能需要设置代理

go env -w GOPROXY=https://goproxy.io,direct

如果报冲突错误,可以直接 set 环境变量

set GOPROXY=https://goproxy.io,direct

查看 go 的环境变量

go env

再执行 get 命令就成功了

go: downloading github.com/petermattis/goid v0.0.0-20180202154549-b0b16
15b78e5
go get: added github.com/petermattis/goid v0.0.0-20180202154549-b0b1615
b78e5

go.mod 会被自动改动,多了安装的这个模块

module calculator

go 1.17

require github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect

并且会多一个 go.sum 文件

github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=

get 的时候可以指定版本

go get github.com/google/uuid@v1.0.0

go.mod 变成

module calculator

go 1.17

require (
    github.com/google/uuid v1.0.0 // indirect
    github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
)

go.sum 变成

github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=

如果包已经存在,然后 go get 指定了新的版本,相当于升降级,取消就用 @none

如果不指定参数,就会自动扫描代码,把需要的三方包,都添加进来,相当于对每个包做 go get 操作

# 不指定参数,添加代码 import 的三方包
go get

依赖包被下载到 $GOPATH/pkg/mod/cache/download
代码被下载到 $GOPATH/pkg/mod


go.sum 和 go.mod 比多了个 hash 值,这是包的 hash 值,用于校验,防止下载的包或本地的包不正常,比如被篡改过

go.sum 用于保证开发使用的依赖,和实际使用的依赖是一致的

如果有配置 GOSUMDB 变量比如 GOSUMDB=sum.golang.org 那么 go 除了校验 go.sum 的值,还会去 GOSUMDB 做二次校验

go.mod 只记录直接依赖的版本,而 go.sum 记录所有用到的依赖的版本

go run, go build, go install

go run 编译并运行程序,不会生成可执行文件

go run main.go

如果 main.go 直接调用同目录下另一个文件的大写开头的变量或函数

package main

func main() {
    Test()
}

其中 Test() 也是 package main 的

package main

func Test() {
}

这样直接运行 go run main.go 会报错

.\main.go:4:5: undefined: Test

必须运行整个目录

go run .\

这样就能运行了

go build 会编译并生成可执行文件(如果有 main 函数的话)

go build

go install 把编译的包放到 $GOPATH/pkg 把生成的可执行文件放到 $GOPATH/pkg

go install

和 build 比多一步包编译和安装

并发协程 goroutine

线程是操作系统调度的最小单位,通常为了并发执行请求,会启动多个线程

这种做法在请求少的时候没问题,当并发请求量很大时,就不适用,因为大量线程的频繁切换,会严重损耗性能

为了解决这种问题,引入了协程的概念

就是线程在执行一个请求中,如果遇到 IO 等操作,不是切换其他线程执行,而是可以继续执行其他就绪的请求

就是把协程放到队列,线程空闲时就挑就绪的协程执行,遇到 IO 操作,就挂起协程,然后挑其他协程执行,挂起的协程就绪再放回队列

这样即充分利用了多核 CPU 做并发操作,又避免了频繁切换线程,大大提高了性能和并发量

其他语言都是通过开发三方包来实现这样的功能

而 Go 语言天然支持协程,只需要通过 go 关键字来开启 goroutine 即可

Go 语言能控制线程调度协程

import (
    "syscall"
    "github.com/petermattis/goid"
)

func main() {
    fmt.Printf("main tid : [%d], gid : [%d]\n", GetThreadId(), goid.Get())

    for i := 1; i <= 5; i++ {
        go gofunc(i)
    }

    // 如果主程序退出,协程也会退出
    time.Sleep(time.Duration(5) * time.Second)

    fmt.Printf("main tid : [%d], gid : [%d]\n", GetThreadId(), goid.Get())
}

func gofunc(functionId int) {
    for i := 1; i <= 3; i++ {
        fmt.Printf("go func [%d], tid : [%d], goid : [%d]\n",
            functionId, GetThreadId(), goid.Get())

        time.Sleep(time.Duration(1) * time.Second)
    }
}

func GetThreadId() int {
    var kernel32Dll *syscall.DLL
    var GetCurrentThreadIdProc *syscall.Proc
    var err error

    kernel32Dll, err = syscall.LoadDLL("Kernel32.dll")
    if err != nil {
        fmt.Printf("syscall.LoadDLL fail: %v\n", err.Error())
        return -1
    }

    // "GetCurrentThreadId" 这个名字不能改,应该是 Kernel32.dll 里的命令
    GetCurrentThreadIdProc, err = kernel32Dll.FindProc("GetCurrentThreadId")
    if err != nil {
        fmt.Printf("kernel32Dll.FindProc fail: %v\n", err.Error())
        return -1
    }

    var pid uintptr
    pid, _, err = GetCurrentThreadIdProc.Call()

    return int(pid)
}

结果

main tid : [22752], gid : [1]
go func [1], tid : [22744], goid : [19]
go func [5], tid : [22752], goid : [23]
go func [4], tid : [22752], goid : [22]
go func [2], tid : [12620], goid : [20]
go func [3], tid : [14964], goid : [21]
go func [4], tid : [22744], goid : [22]
go func [5], tid : [14964], goid : [23]
go func [1], tid : [12620], goid : [19]
go func [2], tid : [15288], goid : [20]
go func [3], tid : [14964], goid : [21]
go func [3], tid : [22752], goid : [21]
go func [2], tid : [14964], goid : [20]
go func [5], tid : [30092], goid : [23]
go func [4], tid : [22744], goid : [22]
go func [1], tid : [15288], goid : [19]
main tid : [15288], gid : [1]

线程和协程不是固定的一一对应关系,每个线程都可以调用任意一个协程

main 也是一个协程,同样可以被不同的线程调用

通道 (channel)

channel 可以用于协程间的通信

// 创建一个传递 int 数据的 channel, 缓冲区为 100
ch := make(chan int, 100)

// 发送消息到 channel
ch <- msg

// 从 channel 读取消息
msg := <-ch

如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值

如果通道带缓冲区,发送方不会阻塞,除非缓冲区满了

接收方在有值可以接收之前会一直阻塞

package main

import (
    "fmt"
    "time"
)

var channel = make(chan int, 5)

func main() {
    go producer()
    go consumer()

    fmt.Println("start producer and consumer")
    time.Sleep(time.Duration(5) * time.Second)
}

func producer() {
    for i := 0; i <= 10; i++ {
        channel <- i
        fmt.Printf("produce %d\n", i)
    }
}

func consumer() {
    for i := 0; i <= 10; i++ {
        msg := <-channel
        fmt.Printf("consume %d\n", msg)
        if i%3 == 0 {
            time.Sleep(time.Duration(1) * time.Second)
        }
    }
}

结果

start producer and consumer
produce 0
produce 1
produce 2
produce 3
produce 4
produce 5
consume 0
consume 1
consume 2
consume 3
produce 6
produce 7
produce 8
consume 4
consume 5
consume 6
produce 9
produce 10
consume 7
consume 8
consume 9
consume 10

可以在读取数据的时候判断有没有出错

// ok 是 bool 类型
v, ok := <-ch

关闭通道

close(ch)

遍历通道

package main

import (
    "fmt"
    "time"
)

var channel = make(chan int, 5)

func main() {
    go producer()

    // 遍历通道
    for i:= range channel {
        fmt.Printf("consume %d\n", i)
    }

    time.Sleep(time.Duration(5) * time.Second)
}

func producer() {
    for i := 0; i <= 10; i++ {
        channel <- i
        fmt.Printf("produce %d\n", i)
    }

    // 关闭通道
    close(channel)
}

结果

produce 0
produce 1
produce 2
produce 3
produce 4
produce 5
consume 0
consume 1
consume 2
consume 3
consume 4
consume 5
consume 6
produce 6
produce 7
produce 8
produce 9
produce 10
consume 7
consume 8
consume 9
consume 10

如果 producer 里面没有 close channel 的话,range 会阻塞等待下一个消息,可是已经没有线程会发消息了,会报死锁错误

web framework

按 github star

  • gin-gonic/gin - 51k
  • beego/beego - 26k
  • kataras/iris - 21k
  • labstack/echo - 20k
  • gorilla/mux - 15k

gin 看起来比较流行

go gin

安装

go get -u github.com/gin-gonic/gin

例子

package main

import (
    "encoding/json"
    "fmt"
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    // group 定义的 url 有相同的前缀
    v1 := router.Group("/v1")
    {
        v1.POST("/login", loginHandler)

        // :groupId 表示这个是必须有的变量
        // 可以匹配 /service/groupA
        // 不能匹配 /service/ 或 /service
        v1.GET("/service/:groupId", listServiceHandler)

        // 请求 /service/group 的时候,不会和上一个 /service/:groupId 的 handler 冲突
        v1.GET("/service/group", listGroupHandler)

        // url 可以有多个可变值
        v1.PUT("/service/:groupId/:serviceId", addServiceHandler)

        // *serviceId 表示这个是可有可无的变量
        // 可以匹配 /service/groupA/ 或 /service/groupA/serviceB
        v1.DELETE("/service/:groupId/*serviceId", deleteServiceHandler)
    }

    // 单独定义一个 url
    router.GET("/status", statusHandler)

    fmt.Println("start server")

    // 启动
    router.Run(":8080")
}

var db = make(map[string][]string)

type User struct {
    Name     string `json:"name"`
    Password int64  `json:"password"`
}

func loginHandler(c *gin.Context) {
    // 按 json 格式获取 body 的值
    json := User{}
    c.BindJSON(&json)

    fmt.Println(json)

    // 返回状态和内容
    c.String(http.StatusOK, fmt.Sprintf("\nHello %s\n", json.Name))
}

func listServiceHandler(c *gin.Context) {
    // 获取路径参数
    group := c.Param("groupId")

    services, ok := db[group]

    if !ok {
        c.String(http.StatusNotFound, "group not exist")
        return
    }

    data, _ := json.Marshal(services)

    c.String(http.StatusOK, string(data))
}

func listGroupHandler(c *gin.Context) {
    // 获取 query 参数
    includeEmptyGroup := c.Query("includeEmptyGroup")

    keys := make([]string, 0)
    for k := range db {
        if len(db[k]) == 0 {
            if includeEmptyGroup != "yes" {
                continue
            }
        }

        keys = append(keys, k)
    }

    data, _ := json.Marshal(keys)

    c.String(http.StatusOK, string(data))
}

func addServiceHandler(c *gin.Context) {
    group := c.Param("groupId")
    service := c.Param("serviceId")

    _, ok := db[group]
    if !ok {
        db[group] = make([]string, 0) // 初始化长度
    }

    db[group] = append(db[group], service)

    c.String(http.StatusOK, "\nadd service successfully\n")
}

func deleteServiceHandler(c *gin.Context) {
    group := c.Param("groupId")
    service := c.Param("serviceId")

    _, ok := db[group]
    if !ok {
        c.String(http.StatusNotFound, "group not exist")
        return
    }

    // 收到的 service 带有 / 符号,即 /xxxx
    fmt.Printf("receive service %s!\n", service)

    service = service[1:]

    found := false
    if service == "" {
        delete(db, group)
        found = true
    } else {
        lenght := len(db[group])
        for i := 0; i < lenght; i++ {
            if service == db[group][i] {
                db[group] = append(db[group][:i], db[group][i+1:]...)  // ... 是必须的
                found = true
                break
            }
        }
    }

    if found {
        c.String(http.StatusOK, "del successfully")
    } else {
        c.String(http.StatusNotFound, "service not exist")
    }
}

func statusHandler(c *gin.Context) {
    c.JSON(200, gin.H{
        "message": "pong",
    })
}

测试

> curl -X POST localhost:8080/v1/login -d '{"name":"lin", "password":123}'
Hello lin

> curl -X PUT localhost:8080/v1/service/group_A/service_1
> curl -X PUT localhost:8080/v1/service/group_B/service_2
> curl -X PUT localhost:8080/v1/service/group_B/service_3
> curl -X PUT localhost:8080/v1/service/group_C/service_4
add service successfully

> curl -X DELETE localhost:8080/v1/service/group_C/service_4
del successfully

> curl -X GET localhost:8080/v1/service/group
["group_A","group_B"]

> curl -X GET localhost:8080/v1/service/group?includeEmptyGroup=yes
["group_A","group_B","group_C"]

> curl -X GET localhost:8080/v1/service/group_A
["service_1"]

> curl -X GET localhost:8080/v1/service/group_B
["service_2","service_3"]

> curl -X GET localhost:8080/v1/service/group_C
[]

> curl -X GET localhost:8080/v1/service/group_D
group not exist

> curl -X DELETE localhost:8080/v1/service/group_D/service_4
group not exist

> curl -X DELETE localhost:8080/v1/service/group_C/service_4
service not exist

> curl -X DELETE localhost:8080/v1/service/group_B/
del successfully

> curl -X GET localhost:8080/v1/service/group
["group_A"]

> curl -X GET localhost:8080/v1/service/group?includeEmptyGroup=yes
["group_A","group_C"]

> curl -X GET localhost:8080/status
{"message":"pong"}

和 springboot 比感觉还差了一些

posted @ 2021-09-03 23:54  moon~light  阅读(159)  评论(0编辑  收藏  举报