Dreamer2q Blog
见到,不如不见
Dreamer2q

Code is cheap, talk is expensive

64日志

Go学习小结

创建于 2021-02-15 共 8874 字,阅读约 35 分钟 更新于 21-02-15 13:46
浏览 31评论 0

前言


这里总结我学习Go 语言的知识点,

可能比较凌乱,

image

因为我学习的时候也是很凌乱的。


Go 语言对其它大众语言做了减法,这使得初学者可以很快的上手 Go 语言。

快速上手的好处就是,初学者很快用 Go 语言做出一些有用的东西,这得益于 Go 语言丰富的包支持。

此外,Go 语言的一大亮点就是语法简单,易懂。

我非常喜欢这种less is more观念,尽管 Go 没有明确的面向对象概念,但是 Go 使用结构体绑定方法来实现面向对象。


这样的好处就是简洁明了,我觉得 C++的面向对象就是语法糖,Go 语言直接在语法层面展示面向对象的实现方法,减少了学习的负担。

所有 Go 语言的结构体真是是 Go 的一大法宝,但除此之外 Go 更优秀的地方在于它的goroutine

一个go关键字就很好的诠释了 Go 语言,可以说,不了解goroutine相当于没有学过Go


本笔记所属内容大多数来源<<Go程序设计实践>>这本书,目录安排也是参考此书。


程序结构


2.3 变量


2.3.4 变量的生命周期


Go 由于自动垃圾回收,变量的回收取决于变量是否可以被访问到

变量存在的位置(堆/栈),不是由变量申明的位置决定(C 语言)。


//变量逃逸
var global *int
func f(){
    var x int
    x = 1
    global = &x //x变量逃逸,使用堆空间
}


2.4 赋值


2.4.1 多重赋值


go 函数支持多个返回值,变量支持多重赋值。


x,y = y,x //多重赋值,右边表达式先被推演
func gcd(x,y int) int {     //求两个整数的最大公约数
    for y != 0 {
        x,y = y, x%y
    }
    return x
}


表达式较复杂时不建议使用多重赋值


2.4.2 可赋值性


赋值要求:变量和值的类型一样


在 C 语言中,赋值比较灵活,编译器会做响应的转换,例如将float赋值给double,但是反过来就会存在问题。


Go 语言中就比较严格,对于var offset int = x == y就是非法的,因为boolint类型不一样。

此外,Go 不支持 C 语言的三元表达式condition ? a : b;


2.5 类型声明


类型声明即定义一个新的类型(可以理解为命名类型),用于区分使用底层类型而造成的混淆,或者隐藏具体的实现?


任何情况下,运行时的转换不会失败。(这句话没理解)


底层类型决定了它的结构,表达方式操作集合


2.6 包和文件


一个包 => 命名空间


补充 fmt 格式化输出


占位符


  • 一般占位符
符号说明
%v相应值的默认格式
%+v打印结构体,默认格式,会添加字段名
%+v相应值的 Go 语法表示
%T相应值类型的 Go 语法表示
%%打印百分号%


  • 布尔占位符
符号说明
%t单词 true 或者 false


  • 整数占位符
符号说明
%b二进制表示
%c相应 Unicode 码所表示的字符
%d十进制表示
%o八进制表示
%q单引号围绕的字符值直面量,由 Go 语言安全转义
%x十六进制小写
%X十六进制大小
%UUnicode 格式:U+1234,等同于"U+%04X"


  • 浮点及其复合构成占位符
符号说明
%b无小数部分的,指数为二的幂的科学计数法,与 strconv.FormatFloat 的 'b' 转换格式一致。例如 -123456p-78
%e科学计数法
%E同上
%f有小数点而无指数
%g根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的 0)输出
%G根据情况选择 %E 或 %f 以产生更紧凑的(无末尾的 0)输出


  • 字符串与字节切片占位符
符号说明
%s字符串或切片的无解译字节
%q双引号围绕的字符串,由 Go 语法安全地转义
%x十六进制,小写字母,每字节两个字符
%X十六进制,大写字母,每字节两个字符


  • 指针
符号说明
%p十六进制表示,前缀 0x


  • 其它标记
符号说明
+总打印数值的正负号;对于 %q(%+q)保证只输出 ASCII 编码的字符
-在右侧而非左侧填充空格(左对齐该区域)
#备用格式:对八进制添加前导 0(%#o),对十六进制添加前导 0x(%#x)或 0X(%#X)对 %p(%#p)去掉前导 0x


数据类型


数据类型时一个语言的基础,我们写程序大多数都是在和数据打交道,因此掌握好语言的数据类型,学起来才能事半功倍。


  • 基础类型(basic type)
    • 数字(number)
    • 字符串(string)
    • 布尔型(boolean)
  • 聚合类型(aggregate type)
    • 数组(array)
    • 结构体(struct)
  • 引用类型(reference type)
    • 指针(pointer)
    • slice
    • map
    • 函数(function)
    • 通道(channel)
  • 接口类型(interface type)
  • 操作符
    • &^ 按位清除
    • ^ 一元时,按位取反(与 C 语言~区别)


3.1 整数



3.2 浮点数



3.3 复数



3.4 布尔值


布尔值无法隐性转换为数值,反过来也不行。


3.5 字符串 !


字符串是不可变的字节序列,这点要与 C 语言区分。


字符串可以包含任意数据,包括 0 值字节,习惯上,文本字符被解读为 UTF-8 编码的的 Unicode 码点序列。


内置len函数返回字符串的字节数,为文字符号数目,因为一个文字符号可能占多个字节。


字符串值不可改变,意味着两个字符串能安全共同使用同一段底层内存,使得复制任意长度的字符串开销低廉。


3.5.1 字符串字面量


字符串字面量(string literal),形式上是带双引号的字节序列。


转义字符以\开始,常见的转义字符如下


  • \a
  • \b
  • \f 换页符
  • \n 换行符(跳到下一行的同一位置)
  • \r 回车符(返回首行)
  • \t
  • \v
  • \'
  • \"
  • \\
  • \xHH 十六进制
  • \ooo 八进制


原生字符串,使用反引号``,此时转义字符无效,可以展开写成多行,例如正则表达,HTML 模板就需要这样的写法。


单引号括起来的字符表示一个rune类型的字符。


3.5.2 Unicode !


需要弄清缘由,程序员不懂不好意思说学过编程。


3.5.3 UTF-8


UTF-8 以字节(一个字节 8 位)为单位对Unicode码点进行变成编码。


UTF-8 由 Go 的两位创建者Ken ThompsonRob Pike发明。(难怪 Go 的源代码都是 UTF-8 格式)


一个字符号的编码的首字节的高位指明了后面还有多少个字节。


  • 0xxxxxxx
  • 110xxxxx 10xxxxxx
  • 1110xxxx 10xxxxxx 10xxxxxx
  • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx


缺点,无法按照下表[i]来访问第i个字符。


优点


  • 兼容 ASCII,自同步
  • 前缀编码,从左到右解码
  • 不嵌入 NUL 字节


逐个处理 Unicode 字符


import "unicode/utf8"

s := "hello,中国"

for i:=0;i<len(s); {
  r,size := utf8.DecodeRuneInString(s[i:])
  fmt.Printf("%d\t%c\n",i,r)
  i += size //跳到下一个字符
}

//因为Go原生支持UTF-8,可以用for range 来遍历,同slice遍历一样

for index,value := range {
  fmt.Printf("%d\t%c\n",index,value)
}

//统计字符串长度
length := utf8.RuneCountInString(s)


由于string类型可以存储任意字节,所有会产生不符合utf8编码的字符出现,此时会产生一个专门的 Unicode 字符\uFFFD(�)替换它。


3.5.4 字符串和字节 slice


3.5.5 字符串和数字的相互转换


3.6 常量


3.6.1 常量生成器 iota


3.6.2 无类型常量


4 复合数据类型


4.1 数组


数组的长度是固定的,同 C 语言,一般很少使用。Slice 的长度可以改变,使用更多。



var q [3]int = [3]int{1,2,3} //定义一个int类型数组,有三个元素(1,2,3)
var p = [...]int{4,5,6} //...表示数组长度由编译器推到
var t = [...]int{1:0, 2:10, 10:1} //指定索引可以个性化赋值

c1 := sha256.Sum256([]byte("x"))
c2 := sha256.Sum256([]byte("X"))
fmt.Printf("%x\n%x\n%t\n%T\n",c1,c2,c1==c2,c1)

/*
2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015
false
[32]uint8
*/


数组长度是类型的一部分,因此[3]int[4]int是不同的类型。


如果数组元素类型可以比较,那么可以使用==来比较两个数组值是否完全相同。


在传递参数时,数组在 Go 作为值传送,需要发生赋值。(区别与 C 语言)


4.2 slice


slice 有三个属性,指针长度容量


len,cap分别返回一个 slice 的长度和容量。


slice 操作符s[i:j]创建了一个新的 slice,其中s可以是数组,指向数组的指针,或者 slice。

s[:]引用了整个数据。通过[i:j]创建的 slice 与之前的 slice 有共同的底层数据。


slice 无法进行比较,不能使用==测试两个 slice 是否相同。(原因:为了安全)


//将一个slice左移n个元素,使用三次反转。
a := []int{0,1,2,3,4,5}
reverse(s[:2])
reverse(s[2:])
reverse(s)


slice 的零值是nil,意味着使用var i []T创建的 slice 不能直接使用,因为它没有对于的底层数据。


可以使用make([]T,len,cap)来创建一个 slice。


4.2.1 append 函数


添加元素到 slice 后面,由于 append 函数添加函数的时候,底层数据可能发送了改变,所有一般将append返回的值赋值给原 slice 变量,确保 slice 可用。


4.2.2 slice 就地修改


就地修改不涉及到底层数据重新分配内存。


//去除空元素
func nonEmpty(strings []string) []string {
  out := strings[:0] //0长度的slice
  for _, s := range strings {
    if s != "" {
      out = append(out,s)
    }
  }
  return out
}


slice 可以用来实现 stack


//push v
stack = append(stack,v)
//top
top := stack[len(stack)-1]
//pop
stack = stack[:len(stack)-1]


4.3 map


map 实现-


map 类型为map[K]V,其中K是键类型,要求必须可以使用==比较。V是值类型,没有任何限制。


delete可以从 map 中根据键删除一个元素,键不存在也无妨。


无法获取一个 map 元素的地址。(原因是后面的操作可能会使地址失效)


使用for k,v := range mapXX迭代循序是不固定的,这是 feature


Go 没有提供集合类型,但是 map 的key是唯一的。


4.4 结构体


Go 的结构体和 C 语言相似。


但是对于一个指向结构体指针的变量,若要引用其结构体成员,Go 只需用.操作符,相当于 C 语言中的->


//二叉树,插入排序

type tree struct {
  value int
  left,right *tree
}

func Sort(values []int) {
  var root *tree
  for _, v := range values {
      root = add(root,v)
  }
  appendValues(values[:0],root)
}

func add(t *tree,value int) *tree {
    if t == nil {
        t = new(tree)
        t.value = value
        return t
    }
    if value < t.value {
        t.left = add(t.left,v)
    }else {
        t.right = add(t.right,v)
    }
    return t
}

func appendValues(values []int,t *tree) []int{
    if t != nil {
        values = appendValues(values,t.left)
        values = append(values,t.value)
        values = appendValues(values,t.right)
    }
    return values
}


4.4.1 结构体字面量


结构体类型的值可以通过结构体字面量来设置。


4.4.3 结构体嵌套和匿名成员


不同寻常的结构体嵌套机制(继承吗?)


Go 允许我们定义不带名称的结构体成员,只需用指定类型(类型必须是命名类型或者指向命名类型的指针)即可;这种结构体成员称作匿名成员


虽然这样可以方面我们访问结构体的成员变量但是,结构体字面量就没办法了,必须按部就班。


Go 中,组合是面向对象编程方式的核心。


4.5 JSON


4.6 模板


函数


5.6 匿名函数


func squares() func() int {
    var x int
    return func() int {
        x++
        return x*x
    }
}
func main(){
    f := square()
    fmt.Println(f()) // 1
    fmt.Println(f()) // 4
    fmt.Println(f()) // 9
    fmt.Println(f()) // 16
}


var prereqs = map[string][]string{
    "algorithms":           {"data structures"},
    "calculus":             {"linear algebra"},
    "compilers":            {"data structures", "formal languages", "computer organization"},
    "data structures":      {"discrete math"},
    "discrete math":        {"intro to programming"},
    "databases":            {"data structures"},
    "formal languages":     {"discrete math"},
    "networks":             {"operating systems"},
    "operating systems":    {"data structures", "computer organization"},
    "programming language": {"data structures", "computer organization"},
}

func main() {
    for i, v := range topoSort1(prereqs) {
        fmt.Println(i+1, v)
    }
}

func topoSort1(m map[string][]string) []string {
    var order []string
    seen := make(map[string]bool)
    var visitAll func(items []string)
    visitAll = func(items []string) {
        for _, item := range items {
            if !seen[item] {
                seen[item] = true
                visitAll(m[item])
                order = append(order, item)
            }
        }
    }

    var keys []string
    for key, _ := range m {
        keys = append(keys, key)
    }
    sort.Strings(keys)
    visitAll(keys)
    return order
}


WARN: 捕获迭代变量


捕获迭代变量可能会造成意想不到的结果,因为捕获的迭代变量将在下一场迭代更新,因此之前捕获的迭代变量内容已经不是原来的东西了,如果需要当时特定的状态,可以在每次迭代中创建一个变量并用迭代变量赋予初始值,这样确保捕获的变量不会发送改变。或者,可以在声明匿名函数的时候直接指明参数,调用的时候传递参数即可,但只适用值传递的类型。


var rmdirs []func()
for _,d := range tempDirs(){
    dir := d    //这个是必需的,不能在匿名函数里面捕获d变量。
    os.MkdirAll(dir,0755)
    rmdirs = append(rmdirs, func(){
        os.RemoveAll(dir)
    })
}


5.7 变长函数


func sum(vals ...int) int {
    total := 0
    for _,val := range vals {
        total += val
    }
    return total
}

func main(){
    fmt.Println(sum())
    fmt.Println(sum(1,2,3,4))
    fmt.Println([]int{1,2,3,4}...)
}


5.8 延迟函数 defer


延迟函数不限制数量,以倒序执行。


func trace(msg string) func() {
    start := time.Now()
    log.Printf("Enter: %s",msg)
    return func() {
        log.Printf("Exit: %s (%s)",msg,time.Since(start))
    }
}

func main() {
    defer trace("Main")()
    time.Sleep(10*time.Second)
}


5.9 宕机


运行时错误,比如数组越界访问,解引用空指针等,都会发生宕机


宕机发生时,程序执行终止,goroutine 延迟函数会执行,程序异常退出留下一条日志消息。

每一个 goroutine 都会在宕机的时候显示一个函数调用的栈跟踪消息。


5.10 恢复


条件


  • 延迟函数内部
  • 宕机与延迟函数在同一个函数内


func main(){
    defer tryToRecover()
    panicMe()
    fmt.Println("It should not be run")
}

func tryToRecover() {
    r := recover()
    if r != nil {
        fmt.Println(r)
        fmt.Println("Try to recover")
        var stack [4096]byte
        runtime.Stack(stack[:], false)
        fmt.Printf("%s\n", stack)
    }else{
        fmt.Println("No neeed to recover")
        fmt.Println(runtime.Caller(1))
    }
}

func panicMe() {
    defer tryToRecover()
    panic("Panic test")
}


有些情况下是没有恢复动作的,比如内存耗尽。


方法


6.4 方法变量


我们可以将一个特定类型变量的方法赋值给一个变量,这个变量叫做方法变量(有点类使用匿名函数捕获变量)。

但我们也可以将一个特定类型的方法赋值给一个变量,这叫做方法表达式


type Point struct{
  X,Y float64
}

func (p *Point) Distance(q Point) float64 {
  return math.Hypot(q.X-p.X,q.Y-p.Y)
}

p := Point{1,2}
q := Point{4,6}

distanceFromP := p.Distance   //方法变量,捕获变量p
distance := Point.Distance //方法表达式,调用的时候需要传入变量p

distanceFromP(q)
distance(p,q)


接口


接口类型是对其它类型行为的概括与抽象。


7.1 接口即约定


接口时一种抽象类型,(隐藏隐藏了数据的布局或者内部结构)仅仅提供一些方法。


//示例,使用接口实现单词统计,Byte计数
type TextCounter struct {
    Words   int
    Blanks  int
    Symbols int
    Length  int
}

func (t *TextCounter) Write(p []byte) (int, error) {
    s := bufio.NewScanner(bytes.NewBuffer(p))
    s.Split(bufio.ScanWords)
    w := 0
    for s.Scan() {
        w++
    }
    t.Words += w
    return len(p), nil
}

func (t TextCounter) String() string {
    return fmt.Sprintf("Words %d", t.Words)
}

type CountingWriter struct {
    bytesCounter int64
    r            io.Writer
}

func (c *CountingWriter) Write(p []byte) (int, error) {
    rlen, err := c.r.Write(p)
    if err == nil {
        c.bytesCounter += int64(rlen)
    }
    return rlen, err
}

func NewCounterWriter(r io.Writer) (io.Writer, *int64) {
    c := &CountingWriter{
        r: r,
    }
    return c, &c.bytesCounter
}

func main() {
    var counter TextCounter
    in := bufio.NewScanner(os.Stdin)
    w,pLen := NewCounterWriter(&counter)
    for in.Scan() {
        fmt.Fprintf(w,in.Text())
    }
    fmt.Println(counter)
    fmt.Println("CountingWriter",*pLen)
}


7.2 接口类型


一个接口类型定义了一套方法,如果一个具体类型要实现改接口,那么必须实现改接口类型定义中的所有方法。


一个 io.Writer 接口抽象了所有可以写入字节的类型,包括文件,内存缓冲区,网络连接等等。


7.3 实现接口


如果一个类型实现了一个接口要求的所有方法,那么这个类型就实现了这个接口,就可以给赋值给接口。


//声明,*Bytes.Buffer实现了io.Writer接口
var _ io.Writer = (*Bytes.Buffer)(nil)


7.4 使用 flag.Value 来解析参数


type Celsius float64
type Fahrenheit float64
type Kelvin float64

const (
    AbsoluteZeroC Celsius = -273.15
    FreezingC     Celsius = 0
    BoilingC      Celsius = 100
)

const (
    celsiusSymbol1    string = "℃"
    celsiusSymbol2    string = "C"
    fahrenheitSymbol1 string = "℉"
    fahrenheitSymbol2 string = "F"
    kelvinSymbol      string = "K"
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
func KToC(k Kelvin) Celsius     { return Celsius(k) - AbsoluteZeroC }
func CToK(c Celsius) Kelvin     { return Kelvin(c + AbsoluteZeroC) }

func (c Celsius) String() string    { return fmt.Sprintf("%g%s", c, celsiusSymbol1) }
func (f Fahrenheit) String() string { return fmt.Sprintf("%g%s", f, fahrenheitSymbol1) }

func (k Kelvin) String() string {
    if k >= 0 {
        return fmt.Sprintf("%g%s", k, kelvinSymbol)
    }
    return fmt.Sprintf("Kelvin %g: out of range", k)
}


下面是使用 flag 包来,来实现自定义解析内容


type celsiusFlag struct {
    Celsius
}

func (f *celsiusFlag) Set(s string) error {
    var unit string
    var value float64
    fmt.Sscan(s, "%f%s", &value, &unit)
    switch unit {
    case celsiusSymbol1, celsiusSymbol2:
        f.Celsius = Celsius(value)
    case fahrenheitSymbol1, fahrenheitSymbol2:
        f.Celsius = FToC(Fahrenheit(value))
    case kelvinSymbol:
        f.Celsius = KToC(Kelvin(value))
    default:
        return fmt.Errorf("invalid temperature %q", value)
    }
    return nil
}

func CelsiusFlag(name string, valude Celsius, usage string) *Celsius {
    f := celsiusFlag{Celsius: valude}
    flag.CommandLine.Var(&f, name, usage)
    return &f.Celsius
}


7.5 接口值


一个接口类型的值(接口值)其实由两部分:一个具体类型和改类型的一个值

二者称为接口的动态类型动态值


Go 语言是静态类型语言,类型仅仅是一个编译时的概念,所有类型不是一个值。在我们的概念模型中,用类型描述符来提供每个类型的具体信息,比如它的名字和方法。

对于一个接口值,类型部分就用对应的类型描述符来表述。


接口的零值,就是把它的动态类型和值都设置为nil。调用一个 nil 接口的任何方法都会崩溃。


var w io.Writer
w = os.Stdout


w = os.Stdout,这句设置接口 w 的类型*os.File一个指向os.File类型的指针


一般来说,在编译时我们我发知道一个接口值的动态类型会时什么,所有通过接口来做调用必然需要使用动态分发。编译器必须生产一段代码来从类型描述符拿到名为Write的方法地址,再间接调用该方法地址。调用的接收者就是接口值的动态值,即os.Stdout,所以实际效果与直接调用等价。


接口值可以使用==!=操作符来做比较。如果两个接口值都时 nil 或者二者的动态类型完全一致且二者动态值相等(使用动态类型的==来做比较),那么两个接口值相等。


所有接口值可以做map的键,和switch的操作数。


如果在比较两个接口值时,如果两个接口值的动态类型一致,但对应的动态值时不可比较的(比如 slice),那么这个比较会以崩溃的方式失败:


var x interface{} = []int{1,2,3,4}\
fmt.Println(x == x) //失败,[]int 类型无法比较


我们必须小心崩溃的可能性,仅在能确认接口值包含的动态值可以比较时,才比较接口值。


!注意含有空指针的非空接口,做!=nil会是true结果


7.6 使用 sort.Interface 来排序


一个原地排序算法需要知道三个信息:序列长度,比较两个元素的含义以及如何交换两个元素


package sort

type Interface interface{
  Len() int
  Less(i,j int) bool //i,j 是序列元素的下标
  Swap(i,j int)
}


7.7 http.Handler 接口


学习 Go 语言的入门,这个必须要了解的,不然怎么分分钟撸一个服务器出来?


我们先看一下 Handler 接口的定义


package http

type Handler interface{
  ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error


我们写一个简单的示例来,通过构建database来实现http.Handler接口,从而实现我们简易的服务器。


const (
    ListenAddress string = "localhost:8080"
)

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("%.2f$", d) }

type database map[string]dollars

func (d database) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.URL.Path {
    case "/list":
        fmt.Fprintf(w, "<h1>List</h1>\n")
        fmt.Fprintf(w,"<ul>")
        for k, v := range db {
            fmt.Fprintf(w, "<li>%s:%s</li>", k, v)
        }
        fmt.Fprintf(w,"</ul>")
    case "/price":
        item := r.URL.Query().Get("item")
        price, ok := db[item]
        if !ok {
            fmt.Fprintf(w, "Item %s not found", item)
            return
        }
        fmt.Fprintf(w,"Item %s is %s", item, price)
    default:
        w.WriteHeader(http.StatusNotFound)
        fmt.Fprintf(w, "Page not found %s", r.URL)
    }
}

var db = database{
    "sock":  0.2,
    "shoes": 10,
}

func main() {
    log.Fatal(http.ListenAndServe(ListenAddress, db))
}


我们虽然可以继续给 ServerHTTP 添加功能,但是对于一个真时的应用,应当把每部分的逻辑分到独立的函数或方法。

这样如果要处理类似,/image/*.png的请求时候就方便的很多了。


因此,net/http包提供了一个请求多发转发器ServeMux,用来简化 URL 和处理程序之间的关联,一个ServeMux把多个http.Handler组合成单个http.Handler


若对于一个更复杂的应用,多个ServeMux会组合起来,用来处理更复杂的分发需求。


下面是一个使用了ServeMux的示例,写得不是很好,主要是html.template中的 template 如何写比较陌生。


const (
    ListenAddress string = "localhost:8080"
)

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("%.2f$", d) }

type database map[string]dollars

var db = database{
    "sock":  0.2,
    "shoes": 10,
}

var listPage = template.Must(template.New("listPage").Parse(`
<html>
<head><title>List</title></head>
<body>
    <h1>List</h1>
    <table>
        <tr>
            <th>Item</th><th>Price</th><th>Operation</th>
        </tr>
        {{range .Items}}
        <tr>
            <th>{{.Item}}</th><th>{{.Price}}</th><th><a href="#">Buy</a></th>
        </tr>
        {{end}}
    </table>
</body>
</html>
`))

func (d database) list(w http.ResponseWriter, r *http.Request) {
    var page struct{
        Items []struct{
            Item string
            Price string
        }
    }
    for k,v := range d {
        page.Items = append(page.Items, struct {
            Item  string
            Price string
        }{Item: k, Price: v.String()})
    }
    listPage.Execute(w,page)
}
func (d database) price(w http.ResponseWriter, r *http.Request) { /* */}
func (d database) add(w http.ResponseWriter, r *http.Request) {/* */}
func main() {
    mx := http.NewServeMux()
    mx.Handle("/", http.HandlerFunc(db.list))
    mx.HandleFunc("/price", db.price)
    mx.HandleFunc("/add", db.add)
    log.Fatal(http.ListenAndServe(ListenAddress, mx))
}


这里面还会涉及到并发的问题,就是如何保护变量不会被在并发中被错误的写入。

里面的水很深啊。


7.8 error 接口


这恐怕是 Go 语言中使用最多的接口类型了吧?

写一行代码处理一个错误


//哈哈哈,这是我写得最多的代码
//动不动就来一句,爽歪歪啊
if err != nil {
  fmt.Println(err)
}


package errors

func New(text string) error { return &errorString{text} }

type errorString struct { text string}

func (e *errorString) String() string { return e.text }


这是一个完整的errors,竟然只有 4 行代码,有点神奇,我本以为package都会由很多代码的,看了有些简小精悍的包,不需要很多的代码,但确实很是实用。


满足error接口的是*errorString指针,这样是为了每次分配的error实例都互不相等。


fmt.Println(errors.New("EOF") == errors.New("EOF")) //false


7.9 表达式求值器


github仓库


7.10 类型断言


类型断言是作用在接口值上面的操作(不然干嘛要断言?),写出来类似于x.(T)

类型断言会检查作为操作数的动态类型是否满足指定的类型断言,若成功则结果就是x的动态值,类型就是T类型;若失败,则崩溃。

类型断言就是用来从它的操作数中把具体类型的值提取出来的操作。


var w io.Writer
w = os.Stdout
f := w.(*os.File)  //success, f is os.Stdout
c := w.(*bytes.Buffer) //panic


如果断言类型T是一个接口类型,那么类型断言检查 x 的动态类型是否满足T。如果检查成功,动态值并没有提取出来,结果仍然是一个接口值,接口值的类型和值部分也没有更变,只是结果的类型接口为T。换句话说,类型断言是一个接口值表达式,从一个接口类型变为拥有另一套方法接口的类型(通常方法数量变多),但保留了接口值中的动态类型和动态值部分。


如果类型断言出现在需要两个结果的赋值表达式,那么类型断言就不会失败,还是多返回一个bool值指明断言是否成功。


var w io.Writer = os.Stdout
f,ok := w.(*os.File) //success,ok , f is os.Stdout
b,ok := w.(*bytes.Buffer) //panic ,b is nil


7.11 使用类型断言来识别错误


7.12 通过接口类型断言来查询特性


func writeString(w io.Writer,s string)(n int, err error){
    type stringWriter interface{
        WriteString(string) (n int,err error)
    }
    if sw,ok := w.(stringWriter); ok {
        return sw.WriteString(s) //避免了内存复制
    }
    return w.Write([]byte(s)) //分配了临时内存
}


一个具体类型是否满足stringWriter接口仅仅由它拥有的方法来决定,而不是这个类型与一个接口类型之间的关系申明。

这依赖一个假定,即WriteString(s)Write([]byte(s))等效。


7.13 类型分支


接口有两种不同的风格。第一种是突出满足这个接口的具体类型之间的相似性,即强调了方法,而不是具体类型,比如,io.Writer。


第二种风格,是利用接口值能够容纳各种具体类型的能力,它把接口作为这些类型的联合(union)来使用,强调满足这个接口的具体类型,而不是这个接口的方法(经常是没有方法的)。


我们把这种风格接口的使用方式称为`可识别联合(discriminated union)


func sqlQuote(x interface{}){
    if x == nil {
        return "NULL"
    }else if _,ok := x.(int); ok {
        return fmt.Sprintf("%d",x)
    }else if b,ok := x.(bool); ok {
        if b {
            return "TRUE"
        }
        return "FALSE"
    }else if s,ok := x.(string); ok {
        return sqlQuoteString(s)
    }else {
        panic(fmt.Sprintf("unexpected type %T : %v",x,x))
    }
}

switch x.(type) {
    case nil:
    case int,uint:
    case bool:
    case string:
    default:
}


上面代码中switch x.(type)写法简化了一连串的if-else语句。

拓展语句switch x := x.(type),这个新的x就是断言后的类型。

分支类型不支持使用fallthrough

注意:switch 安装顺序(从上到下)执行,遇到符合的case就进去,不会考虑后面的 case


func sqlQuote(x interface{}) string {
    switch x := x.(type) {
        case nil:
            return "NULL"
        case int,uint:
            return fmt.Sprintf("%d",x)
        case bool:
            if x {
                return "TRUE"
            }
            return "FALSE"
        case string:
            return sqlQuoteString(x)
        default:
            panic(fmt.Sprintf("unexpected type %T: %v",x,x))
    }
}


7.14 基于标记的 XML 解析


func main() {

    dec := xml.NewDecoder(os.Stdin)
    var stack []string
    for {
        tok, err := dec.Token()
        if err == io.EOF {
            break
        } else if err != nil {
            fmt.Fprintln(os.Stderr, "xmlselect: %v\n", err)
            os.Exit(1)
        }
        switch tok := tok.(type) {
        case xml.StartElement:
            stack = append(stack, tok.Name.Local)
        case xml.EndElement:
            stack = stack[:len(stack)-1]
        case xml.CharData:
            if containsAll(stack, os.Args[1:]) {
                fmt.Printf("%s: %s\n", strings.Join(stack, " "), tok)
            }
        }
    }
}

func containsAll(x, y []string) bool {
    for len(y) <= len(x) {
        if len(y) == 0 {
            return true
        }
        if x[0] == y[0] {
            y = y[1:]
        }
        x = x[1:]
    }
    return false
}


反射


在编译时不知道类型,可更新变量、运行时查看值、调用方法以及直接对它们的布局进行操作,这种机制称为反射


12.2 reflect.Type 和 reflect.Value