Go语言笔记

2016-12-20 Lingxian Kong 更多博文 » 博客 » GitHub »

原文链接 https://lingxiankong.github.io/2016-12-20-go-note.html
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


教程

Go官方文档:https://golang.org/doc/

查看标准库列表:

几个不错的翻译教程:

新手想学 golang,可以先到这里感受一下:https://play.golang.org/

在系统中使用不同版本的Go:GVM 类似于python的virtualenv: gvp

golang 编码格式约定:https://github.com/golang/go/wiki/CodeReviewComments

Go 命令

有一个go可执行程序,如何run?go build会在当前目录生成可执行程序(或者-o <exe路径>),如果是go install,会在$GOPATH/bin/下创建可执行程序。

如果是普通包,go install 会在本机安装包,在别的代码中就可以import了。而go build不会产生任何文件。

goimports 比 gofmt 多一个自动排序 import 的功能

go clean,代码提交前清除当前源码包和关联源码包里面编译生成的文件。 -i 清除关联的安装的包和可运行文件,也就是通过go install安装的文件 -n 把需要执行的清除命令打印出来,但是不执行

测试某个类: go test -v -test.run TestMain /usr/local/go/bin/go test -timeout 30s github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/testing -run ^TestRequiredRuleCreateOpts$

安装和试用

这里有一个安装脚本,安装 go 1.9以及 glide

更新本文时,Go的最新版是1.10,我直接在Ubuntu虚拟机中,下载Go的二进制包,并设置我的环境变量。

apt-get purge -y golang-go # 不能自动跟后面的命令一起执行
mkdir -p /opt/go
export GOROOT=/opt/go # go 的安装目录
export GOPATH=$HOME/go # GOPATH 目录用于存放src/bin/pkg,从1.8版本开始默认是~/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin/

sudo touch /etc/profile.d/go.sh
sudo tee /etc/profile.d/go.sh >/dev/null <<'EOF'
export GOROOT=/opt/go
export GOPATH=$HOME/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin/
EOF

# 到 https://golang.org/dl/ 查看版本
version=1.10.4
source ~/.bashrc && cd /opt && wget https://storage.googleapis.com/golang/go${version}.linux-amd64.tar.gz
tar xzf go${version}.linux-amd64.tar.gz && rm -f go${version}.linux-amd64.tar.gz
# 我执行了这一步,不知道是不是必须的,有个文档说Go 的工具链是用 C 语言编写的,因此在安装 Go 之前你需要先安装相关的 C 工具
apt-get install -y bison ed gawk gcc libc6-dev make
cd -

安装脚本:

curl -SL https://gist.githubusercontent.com/lingxiankong/ad724e785abd133632cc604210b45659/raw/de29e5dbe1ad597891b40c3862a118d05be11da8/install_go.sh -o ~/install_go.sh && source ~/install_go.sh 1.10.4

或者借助 gimme 安装:

eval "$(curl -sL https://raw.githubusercontent.com/travis-ci/gimme/master/gimme | GIMME_GO_VERSION=1.10.4 bash)"

安装完毕,就这么简单。然后就可以编写Go代码了。一个最简单的hello world如下:

package main
func main() {
    println("Hello", "world")
}

保存为hello.go文件,命令行运行go run hello.go,就会看到输出。一些教程还会举个稍微高级的例子,打印Go的版本号:

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Printf("%s\n", runtime.Version())
}

很多教程上来就说Go如何好,但我觉得,如果你真有阅读Go代码的需求(比如我是为了阅读Swarm源码),可略过那些章节,直接学习如何写Go语言即可。因为在很熟悉一门编程语言并有与之相关的项目经验前,那些东西除了吹嘘,没有任何实质意义。

再贴一个最简单的用标准库实现http web server:

package main

import (
    "io"
    "log"
    "net/http"
    "os"
)

func HelloServer(w http.ResponseWriter, req *http.Request) {
    hostname, _ := os.Hostname()
    log.Println(hostname)
    io.WriteString(w, "Hello, world! I am "+hostname+" :)\n")
}

func main() {
    http.HandleFunc("/", HelloServer)
    log.Fatal(http.ListenAndServe(":12345", nil))
}

如下是我自己看教程的一些笔记,没有过目不忘的本事,只能靠反复看笔记了。

先谈谈我自己的感受,大概扫了一遍Go语言基础教程,发现Go其实融合了Java、C、Objective-C、Python等语言的一些特性,学Go的过程中,脑子里一直有好几个小人在打架,是并行的几个线程自我否定的过程,比较痛苦。但多掌握一门编程语言不是坏事,就算是锻炼自己脑子了。如果你先前没有其他编程语言经验,那么恭喜,对于Go语言你会很快上手的。另外,一门编程语言真正强大的是它的库函数,所以教程中有关库函数的讲解其实也可以忽略,因为你真正要用的时候,最好还是翻阅权威的官方文档(Go自带package文档参阅这里,国内用户可以访问这里)。

笔记

点比较散。有些东西当熟悉了Go语言之后再回过头来看,可能会比较简单。

Go 把 runtime 嵌入到了每一个可执行文件当中,所以go可执行文件的运行不需要依赖任何其它文件,它只需要一个单独的静态文件即可(所以导致可执行文件比较大)。

如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。

main包中的main函数既没有参数,也没有返回类型(与C家族中的其它语言恰好相反)

每一个源文件都可以包含且只包含一个 init 函数,init 函数是不能被调用的。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性;也经常被用在当一个程序开始之前调用后台执行的 goroutine. 需要注意,如果某个init 函数内部用 go 关键字启动了新的 goroutine,这个 goroutine 只有在 main.main 函数之后才能被执行到。

可以用import _ <package path>引入一个包,仅仅是为了调用init()函数,而无法通过包名来调用包中的其他函数。 而 import . <package path> 的意思是可以直接引用该包中的函数而不需要包名。

依赖管理

  • v1.7中已经将vendor纳入标准特性中,并废弃GO15VENDOREXPERIMENT环境变量
  • godep的“版本管理”本质上是通过缓存第三方库的某个revision的快照实现的(在 Godeps/_workspace 目录中)
  • vendor,vendor 标准化了项目依赖的第三方库的存放位置。从 go 1.5 开始出现官方的 vendor 支持。对vendor目录的解释,2015.12:https://blog.gopheracademy.com/advent-2015/vendor-folder/,vendor 使用时要注意交叉依赖的问题,即不同的包依赖同一个包的不同版本。但 vendor 仅仅是提供一个目录,缺乏管理机制(比如如何更新)
  • glide, deprecated,比如 fn/fission 项目的依赖管理就是用 glide,关于 glide 的介绍:https://blog.bluerain.io/p/Golang-Glide.html
  • dep,官方出品,目前处于开发和实验阶段,引入两个文件 Gopkg.tomlGopkg.lock,Gopkg.toml 定义都依赖哪些包/branch/version,Gopkg.lock 定义中多了 revision 信息,即具体的 commit id
    • 安装 go get -u github.com/golang/dep/cmd/dep
    • 介绍 2017.06:http://tonybai.com/2017/06/08/first-glimpse-of-dep/
    • 2017.10,https://supereagle.github.io/2017/10/05/golang-dep/
    • dep ensure,根据配置文件下载安装依赖包
    • dep status 查看当前的依赖情况
    • dep ensure -add github.com/golang/glog
    • dep ensure -update,比如修改了Gopkg.toml,该命令更新 lock 文件和 vendor 下的数据使其满足约束。或者直接指定更新的库,dep ensure -update github.com/spf13/pflag
    • 如果要测试自己的 repo代替 Gopkg.toml 中的依赖,在 constraint 中指定 source 为自己的 repo 以及branch,name不变

变量

  • 浮点数的类型有float32和float64两种(没有float类型),默认是float64。
  • var _ I = T{},这个语句其实在测试结构体T是否实现了接口I,下划线忽略变量名
  • reflect
    • 在一个变量上调用 reflect.TypeOf() 可以获取变量的正确类型
    • reflect.ValueOf(s) 获取对象的值
  • 声明数组:var arr1 [5]int,数组的初始化有很多种方式(比如自动计算长度:var arr1 [...]int{1,2,3}),数组是值类型,数组的长度的固定的,可以通过传递数组的引用在函数中修改数据。用 for...range 的方式迭代数组会更安全,每轮迭代对数组元素访问时可以不用判断下标越界。
  • 声明切片:var slice1 []type,不需要说明长度,初始化:var slice1 []type = arr1[start:end],切片是引用类型。切片类似于Python中的list,切片使用make() 创建。切片的cap就相当于数组的长度。切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。当相关数组还没有定义时,可以使用 make() 函数来创建一个切片同时创建好相关数组:var slice1 []type = make([]type, len)
  • 声明 map:var map1 map[string]int,map 是引用类型,用 make() 创建。判断一个map中是否存在某个key:val1, isPresent = map1[key1]。map 不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制
  • new 和 make 的区别:make 只能用于 slice,map,channel 三种内建引用类型。new适用于值类型如数组和结构体的内存分配。new(T) 分配零值填充的T类型的内存空间,返回的是 T 的指针,
  • var x interface{} = nil
  • 对于使用:=定义的变量,如果新变量p与同名已定义变量 (这里就是那个全局变量p)不在一个作用域中时,那么golang会新定义这个变量p,遮盖住全局变量p

函数

  • 函数支持可变参数,func myFunc(a, b, args ...int) {},这里 args 其实是一个切片。如果是传递数组,可以这样myFunc(1, 2, arr...),这样其实是传递数组中每个元素。如果可变参数中值的类型都不一样,可以使用struct或空接口。

  • 函数参数默认值:不支持。Go语言追求显式的表达,避免隐含

  • 实现类似于 python if...in...

  import sort
  func findString(a string, list []string) bool {
    sort.Strings(list)
    index := sort.SearchStrings(list, a)
    if index < len(list) && list[index] == a {
        return true
    }
    return false
  }

程序结构

Go支持的代码结构语句:

  • if-else

定义函数时,最好额外返回一个执行成功与否的变量,如下写法会被经常用到:

  if value, ok := readData(); ok {
  }
  • for/for-range

for就类似于C语言中的结构了。Go不支持while语句,无限循环可以用for { } 代替。 for-range结构是Go独有的,类似于有些语言的for-each,写法是for idx, val := range coll { },需要注意,val 始终为集合中对应索引的值拷贝,是只读的。

对 channel 的遍历:

  for inValue := range in {
  }
  • switch

python 没有 switch。switch无需使用break退出分支,默认行为是匹配到一个分支运行结束后,就退出switch语句。如果希望继续执行,使用fallthrough。

  • select

channel轮询

  • 对数组的遍历两种方式:

    // 第一种
    for i:=0; i < len(arr1); i++ {
        arr1[i] = 1
    }
    // 第二种
    for index,value := range arr1 {
    ...
    }
    
  • recover 只能在 defer 修饰的函数中使用:用于取得 panic 调用中传递过来的错误值,如果是正常执行,调用 recover 会返回 nil,且没有其它效果。

  • Go支持标签和goto语法,但尽可能少用。

结构体

  • P := new(person) 返回的 P 是一个指针
  • 类型对应的方法必须和类型定义在同一个包中。因此无法直接给 int 等类型添加自定义方法。但可以通过自定义类型的方式,比如:type ages int
  • 如果在 struct 的定义中引用了另一个 struct 的类型,会继承这个 struct 的属性和方法
  • 获取一个接口变量varI真正的值,value, ok := varI.(T),简化形式:value := element.(T)。也可以用switch t := varI.(type) 对接口变量的类型进行判断。
  • 测试一个变量v是否实现了某个接口:sv, ok := v.(varI)

结构体和 json

  • 定义可转化为 json 的结构体时,json 字段必须是导出的,如果结构体变量名称和 json 字段不同,可使用成员标签定义
  • json.Marshal 把结构体转换为 []byte 数组
type Move struct {
    Title string
    Year  int `json:"released"`
    Color bool `json:"color,omitempty"`
    Actors []string
    NotParse bool `json:"-"` // 不解析
}
var movies = []Movie{...}
data, err := json.Marshal(movies) // 或者是 MarshalIndent(movies, "", "    ")

同步

  • 为了保证同一时刻只有一个线程会修改一个struct,通常的做法是在struct的定义中加入sync.Mutex类型的互斥锁变量。在函数中,可以通过lock和unlock函数加锁和解锁。在 sync 包中还有一个 RWMutex 读写锁:他能通过 RLock() 来允许同一时间多个线程对变量进行读操作,但是只能一个线程进行写操作。如果使用 Lock() 将和普通的 Mutex 作用相同。

  • sync.WaitGroup 的用法是程序需要并发执行多个goroutine,但需要等所有goroutine都结束才能继续。

  package main

  import (
    "fmt"
    "sync"
  )

  var waitgroup sync.WaitGroup

  func Afunction(shownum int) {
      defer waitgroup.Done()
    fmt.Println(shownum)
  }

  func main() {
    for i := 0; i < 10; i++ {
        waitgroup.Add(1) //每创建一个goroutine,就把任务队列中任务的数量+1
        go Afunction(i)
    }
    waitgroup.Wait() //.Wait()这里会发生阻塞,直到队列中所有的任务结束就会解除阻塞
  }

测试

  func TestSum(t *testing.T) // 定义测试函数
  testing.T的Error, Errorf, FailNow, Fatal, FatalIf方法,说明测试不通过

channel

  • 往 channel 发送数据也必须在协程中,因为 channel 的发送和接收都是阻塞操作且无缓冲。发送操作在接收者准备好之前是阻塞的

  • 关闭之后不能写但可以读,最好在生产者/发送者 close(ch),接收者需要判断 channel 是否已关闭

  • 在 select 中,如果多个可以处理,则随机选择一个

  • 有时候一个函数仅发送数据或接收数据,定义时可以:

  func source(ch chan<- int){
    for { ch <- 1 }
  }
  • 如果不管通道中存放的是什么,只关注长度,可以创建如下通道
  type Empty interface {}
  type semaphore chan Empty
  sem := make(semaphore, N)
  // 基本定义,无缓冲 channel,不限容量
  ch := make(chan int)
  // 有缓冲 channel
  ch := make(chan string, 100)

  // 等待接收数据。如果 channel 关闭了 ok == false
  if v, ok := <- ch; ok {
    process(v)
  }
  // 或者使用for 语句,直到通道被关闭
  for input := range ch {
        process(input)
  }

  // 防止 channel 超时
  package main
  import (
      "fmt"
      "time"
  )
  func main() {
      c := make(chan int)
      o := make(chan bool)
      go func() {
          for {
              select {
              case i := <-c:
                  fmt.Println(i)
              case <-time.After(time.Duration(3) * time.Second):
                  fmt.Println("timeout")
                  o <- true
                  break
              }
          }
      }()

      <-o
  }
  • 测试通道关闭,设置超时
  package main

  import (
    "fmt"
    "time"
  )

  func prepareData(ch chan string) {
    for _, lb := range []string{"lingxian", "kong", "catalyst"} {
        ch <- lb
        time.Sleep(3 * time.Second)
    }

    close(ch)
    fmt.Println("channel closed!")
  }

  func main() {
    ch := make(chan string)
    go prepareData(ch)

    for i := 0; i < 2; i++ {
        go func(ch <-chan string, i int) {
            for {
                select {
                case s, ok := <-ch:
                    if !ok {
                        fmt.Printf("goroutine %d exits\n", i)
                        return
                    }
                    fmt.Printf("goroutine %d: %s\n", i, s)
                    time.Sleep(1 * time.Second)
                case <-time.After(time.Duration(3) * time.Second):
                    fmt.Printf("goroutine %d timeout\n", i)
                    break
                }
            }
        }(ch, i)
    }

    time.Sleep(20 * time.Second)
  }

signal

  • 使用场景:如果希望捕获操作系统信号并做相应的处理(比如优雅停止等)
  • 你只需定义关心哪些信号,如果不设置表示监听所有的信号
sigs := make(chan os.Signal, 0)
done := make(chan bool, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1, syscall.SIGUSR2)

go func(doneCh chan<- bool) {
    sig := <-sigs
    fmt.Println("Got signal:", sig)
    done <- true
}(done)

context

  • 在父子线程间传递,父线程终止时传递信息给子线程也终止

  • 与 signal 配合的一个例子:

  package main

  import (
      "context"
      "fmt"
      "os"
      "os/signal"
      "time"
  )

  func main() {
      ctx, cancel := context.WithCancel(context.Background())

      exitCh := make(chan struct{})
      go func(ctx context.Context) {
          for {
              fmt.Println("In loop. Press ^C to stop.")
              // Do something useful in a real usecase.
              // Here we just sleep for this example.
              time.Sleep(time.Second)

              select {
              case <-ctx.Done():
                  fmt.Println("received done, exiting in 500 milliseconds")
                  time.Sleep(500 * time.Millisecond)
                  exitCh <- struct{}{}
                  return
              default:
              }
          }
      }(ctx)

      signalCh := make(chan os.Signal, 1)
      signal.Notify(signalCh, os.Interrupt)
      go func() {
          select {
          case <-signalCh:
              cancel()
              return
          }
      }()

      <-exitCh
  }

时间相关

  • 计时器(循环触发)

    ticker := time.NewTicker(updateInterval)
    defer ticker.Stop()
    ...
    select {
    case u:= <-ch1:
        ...
    case v:= <-ch2:
        ...
    case <-ticker.C:
        logState(status) // 计时结束
    default: // no value ready to be received
        ...
    }
    
  • 定时器(触发一次),上面例子的 time.After

字符串 string

  • strings介绍

  • 使用fmt.Printf,换行需要加\n%v代表使用类型的默认输出格式,%#v 提供最典型的调试信息,%t表示布尔型,%g表示浮点型,%p表示指针。int 不能使用 %s 打印,必须是%d

  • python 里面的原生字符串(比如r('xxx'))在 go 中用反引号表示。

  • 什么时候用 string 什么时候用*string,参见这里。简单说,使用 string,在unmarshal时如果字段 missing 则赋值为空字符串,而如果使用*string,缺失的字段值为 nil。如果一个字段被 update 的时候可以为空字符串,那么最好设计成*string,否则不指定时就会被自动 update 成空字符串。同样,bool 类型如果不指定默认为 false

  • 字符串拼接除了 Sprintf(返回字符串),还可以使用 strings.Join([]string{s,"ewrwer"},"")

  • Println

  s := fmt.Sprintf("vip port: %s", lb.VipPortID)
  fmt.Println(s)

错误处理

一些技巧

第三方库

日志

  import (
      log "github.com/sirupsen/logrus"
  )
  log.SetOutput(os.Stdout)
  log.SetLevel(log.InfoLevel)
  log.SetFormatter(&log.TextFormatter{})
  // 一般打印
  log.WithFields(log.Fields{"error": err}).Fatal("Unable to decode the configuration")
  log.WithFields(log.Fields{
    "event": event,
    "topic": topic,
    "key": key,
  }).Info("Failed to send event")
  // 或者把变量固定
  requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
  requestLogger.Info("")
  • glog,k8s 使用
  import glog "github.com/golang/glog"
  glog.V(2).Infoln("Processed", nItems, "elements")
  glog.Fatalf("Initialization failed: %s", err)

HTTP 服务 Gorilla Mux

  r := mux.NewRouter()
  r.HandleFunc("/products", ProductsHandler).
    Host("www.example.com").
    Methods("GET").
    Schemes("http")
  • 如果很多routes 有一些重复的 matcher,可以使用 subrouter
  s := r.Host("www.example.com").Subrouter()
  s.HandleFunc("/products/", ProductsHandler)
  • 有了 router,可以结合http.Server
  srv := &http.Server{
      Handler:      r,
      Addr:         "127.0.0.1:8000",
      // Good practice: enforce timeouts for servers you create!
      WriteTimeout: 15 * time.Second,
      ReadTimeout:  15 * time.Second,
  }
  log.Fatal(srv.ListenAndServe())
  • middleware 的定义
  type MiddlewareFunc func(http.Handler) http.Handler 
  // 比如
  func simpleMw(next http.Handler) http.Handler {
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
          // Do stuff here
          log.Println(r.RequestURI)
          // Call the next handler, which can be another middleware in the chain, or the final handler.
          next.ServeHTTP(w, r)
      })
  }
  r.Use(simpleMw)

层级命令行 cobra

https://github.com/spf13/cobra 编写 CLI 程序,可以自动生成 CLI 框架,推荐!

  • 基本用法
// 定义 rootcmd
var rootCmd
var getProjectsCmd = &cobra.Command{}
rootCmd.AddCommand(getProjectsCmd)
// 也可以同时 add 多个
rootCmd.AddCommand(cmdPrint, cmdEcho)
# in .bashrc
export GOPATH=$HOME/go_project
export PATH=$PATH:$GOPATH/bin
# Avoid the error 'Unknown SSL protocol error in connection to gopkg.in'
Lingxians-MAC[~]$ git config --global http.sslVerify true
Lingxians-MAC[~]$ go get -u -v github.com/spf13/cobra/cobra
Lingxians-MAC[~]$ cobra init $GOPATH/src/tests/runit
Lingxians-MAC[~]$ cd $GOPATH/src/tests/runit
Lingxians-MAC[~/go_project/src/tests/runit]$ ll
total 32
-rw-r--r--  1 konglingxian  staff  11358 Jan  6 13:01 LICENSE
drwxr-xr-x  3 konglingxian  staff    102 Jan  6 13:01 cmd
-rw-r--r--  1 konglingxian  staff    677 Jan  6 13:01 main.go
Lingxians-MAC[~/go_project/src/tests/runit]$ cobra add os
os created at /Users/konglingxian/go_project/src/tests/runit/cmd/os.go
# 注意这里不能是‘cobra add get-projects’,cobra 暂时不支持
Lingxians-MAC[~/go_project/src/tests/runit]$ cobra add getProjects -p 'osCmd'
getProjects created at /Users/konglingxian/go_project/src/tests/runit/cmd/getProjects.go
Lingxians-MAC[~/go_project/src/tests/runit]$ tree
.
|____cmd
| |____getProjects.go
| |____os.go
| |____root.go
|____LICENSE
|____main.go
# 然后你自己可以编写逻辑了……

gin

实现 HTTP server 和 API 路径绑定(router),可以与 http.Server 配合实现 http 服务

https://github.com/gin-gonic/gin

fn 和 fission 项目都使用

router := gin.Default()
// 路径参数
router.GET("/user/:name", getUser)
// 可选路径参数
router.GET("/user/:name/*action", userAction)

gophercloud

http://gophercloud.io/docs/compute/#setup

  • 输入是鉴权信息,获取一个 provider
  • 根据 provider 获取各个 service client
  • 导入各个资源的包,调用包的导出函数(service client 为参数)
  • 这种结构很怪,感觉是函数式编程

代码示例

http 服务器

  • handler,任何实现 ServeHTTP 方法的对象都可以作为 Handler,但http.HandleFunc简化了 handler 的编写
  • 预定义了一些通用的 handler,比如FileServer,404的NotFoundHandler,重定向的RedirectHandler
  • 其实就算代码没用 ServerMux,net/http 在后台也会默认创建使用 DefaultServeMux
  • 使用ServerMux时要注意:/images 会匹配 /images/cute-cat.jpg
  • ServerMux 不支持带参数的路由,不能捕获 URL 中的变量,不支持 HTTP 方法匹配。所以才有Gorilla Mux或httprouter库
// 最原始的写法
package main
import (
    "io"
    "net/http"
)
type helloHandler struct{}
func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, world!"))
}
func main() {
    http.Handle("/", &helloHandler{})
    http.ListenAndServe(":12345", nil)
}

// 上述写法要为每个 handler 都定义一个结构体和实现ServeHTTP方法,很不方便,于是net/http提供了HandleFunc方法,允许直接把特定类型的函数作为 handler。其实 http.HandleFunc 第二个参数的定义如下,所以任何满足该参数定义方式的函数都能作为 handler:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

package main
import (
    "fmt"
    "log"
    "net/http"
)
func handler(w http.ResponseWriter, req *http.Request) {
    // io.WriteString(w, "hello, world!\n")
    // w.Write([]byte("Hello, world!"))
    fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
}
func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// 或者使用 ServerMux,允许定义多个 URL 到 handler 的映射。http.ListenAndServe函数第二个参数就是 mux,因为它是个特殊的 handler,也实现了ServeHTTP方法
package main
import (
    "net/http"
    "io"
)
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/bye", func(w http.ResponseWriter, r *http.Request) {
        io.WriteString(w, "byebye")
    })
    mux.HandleFunc("/hello", sayhello)
    http.ListenAndServe(":8080", mux)
}
func sayhello(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "hello world")
}

类似于 python 中的 if…in...

import "sort"
files := []string{"Test.conf", "util.go", "Makefile", "misc.go", "main.go"}
target := "Makefile"
sort.Strings(files)
i := sort.SearchStrings(files, target) // 行为比较怪异,如果 target 不在列表中,则返回 target 应该 insert 的 index
if i < len(files) && files[i] == target {
    fmt.Printf("found \"%s\" at files[%d]\n", files[i], i)
}
// 写成函数
func FindString(a string, list []string) bool {
    if a == "" || len(list) == 0 {
        return false
    }

    sort.Strings(list)
    index := sort.SearchStrings(list, a)
    if index < len(list) && list[index] == a {
        return true
    }
    return false
}

定时器

// Tick 是循环发送,After 只发送一次
tick := time.Tick(1e8)
boom := time.After(5e8)
for {
    select {
    case <-tick:
        fmt.Println("tick.")
    case <-boom:
        fmt.Println("BOOM!")
        return
    default:
        fmt.Println("    .")
        time.Sleep(5e7)
    }
}