当前位置: 首页 > 工具软件 > Bible Reader > 使用案例 >

go语言圣经-笔记

唐弘和
2023-12-01

还没理解

特性

  • 向后兼容
  • 静态编译
  • 不允许无用的变量

基础

命名

函数名,变量名,常量名,类型名

  • 大写字母开头命名是导出的
  • 推荐驼峰命名

声明

包声明:package
函数声明:func
常量声明:const
变量声明:var

变量

声明+赋值:var 变量名字 类型 = 表达式

  • var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。

简短变量声明:变量名字 := 表达式

  • 简短变量声明被广泛用于大部分的局部变量的声明和初始化

类型

type 类型名字 底层类型
type Celsius float64    // 摄氏温度
type Fahrenheit float64 // 华氏温度

布尔型

有短路行为,如果运算符左边的值已经可以确定整个表达式的值,那么右边的值不再被求值。

字符串

字符串是不可修改的,但是可以分配新的值或者追加
十六进制转义:\xhh
八进制转义:\ooo
多行(原生)字符串,没有转义,常用于编写正则表达式:qwe

每个符号对应一个unicode码点,对应go中的rune整数,和int32是等价类型,把符文序列表示为一个int32序列,叫做UTF-32编码,浪费空间。
unicode转义字符:\uhhh,16bit码点;\uhhhhhh,32bit码点
rune字符:\U00004e16 == \u4e16
UTF-8,把unicode码点编码为字节序列的变长编码,使用1-4字节来表示每个unicode码点,但是无法通过索引来访问第n个字符。
“世界”
“\xe4\xb8\x96\xe7\x95\x8c”
“\u4e16\u754c”
“\U00004e16\U0000754c”

go语言的range循环应用于字符串的时候会自动隐式的解码utf-8字符串,对于非ASCII,索引更新会超过1个字节。
UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割。

标准库

  • Bytes:针对[]byte类型
  • Strings:字符串的查询、替换、比较、截断、拆分和合并等功能
  • Strconv:提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换
  • Unicode:提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。

path和path/filepath包提供了关于文件路径名更一般的函数操作。使用斜杠分隔路径可以在任何操作系统上工作

无类型常量

延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。
无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。

const (
    deadbeef = 0xdeadbeef // untyped int with value 3735928559
    a = uint32(deadbeef)  // uint32 with value 3735928559
    b = float32(deadbeef) // float32 with value 3735928576 (rounded up)
    c = float64(deadbeef) // float64 with value 3735928559 (exact)
    d = int32(deadbeef)   // compile error: constant overflows int32
    e = float64(1e309)    // compile error: constant overflows float64
    f = uint(-1)          // compile error: constant underflows uint
)

复合类型

数组

var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
q := [...]int{1, 2, 3}
r := [...]int{99: -1}

固定长度的特定类型元素序列
长度固定所以很少用,大多数使用切片,可以增长和缩短的动态序列。
长度为省略号表示根据值初始化数组

切片

声明时[]内不加内容,声明的就是切片。
底层引用数组,没有固定长度。
三部分组成

  • 指针:指向第一个元素对应的底层数组元素的地址
  • 长度:切片中元素数目,len()
  • 容量:从切片开始到底层数组结尾的位置,cap()

切片的切片会用来创建一个新的切片。
数组之间可以比较,但是切片不能。

Map

// make func 创建
ages := make(map[string]int) // mapping from strings to ints
// 字面值创建
ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}

Map在key查找失败时会返回value类型对应的零值。
不能对Map中的元素取地址。
map类型的零值是nil。不能向nil的map存入数据。很有趣,map只能按上面的方式创建,下面的方式声明map并且存入数据会报错:

var ages map[string]int
ages["why"] = 21 // panic: assignment to entry in nil map

map的下标语法会产生两个值,常用ok作为bool变量,判断是否存在。
if age, ok := ages["bob"]; !ok { /* ... */ }
go语言不存在set,可以把map当set用。
map的key必须是可比较的类型,可以通过辅助函数来转换。

结构体

type Employee struct {
    ID        int
    Name, Address string
    DoB       time.Time
    Position  string
    Salary    int
    ManagerID int
}

var dilbert Employee
dilbert.Salary -= 5000
var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"
(*employeeOfTheMonth).Position += " (proactive team player)"

空结构体的空间大小为0。
用字面值初始化结构体,可以忽略结构体内部的顺序影响。
结构体内部成员可以比较那么结构体也可以比较。

结构体的匿名成员

可以直接访问结构体的叶子属性,而不需要给出完整的路径。
字面值不能用匿名成员。

type Point struct {
    X, Y int
}

type Circle struct {
    Point
    Radius int
}

type Wheel struct {
    Circle
    Spokes int
}
var w Wheel
w.X = 8            // equivalent to w.Circle.Point.X = 8
w.Y = 8            // equivalent to w.Circle.Point.Y = 8
w.Radius = 5       // equivalent to w.Circle.Radius = 5
w.Spokes = 20

w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}

json

编组:json.Marshal,把一个结构体转为json
默认使用Go语言结构体成员名字作为json对象

高级

函数

如果函数没有返回值则执行完毕后不返回任何值。
如果实参包含引用类型,比如:指针,切片,map,func,channel等,实参可能会由于函数的间接引用被修改。
没有函数体的函数声明意味着该函数并不是go实现的。
直接返回:如果函数的返回值都有显式的变量名,return时可以省略返回内容,bare return。(不利于代码理解,用于短的代码段可以,长的就不要用了)

签名

函数的签名就是函数的类型:传入类型一一对应,返回值对应,认为两个函数有相同的签名,变量名不影响签名。

匿名函数

没有函数名的函数,通过函数字面量来定义。
特点:在函数内部定义的内部函数可以访问到函数的变量。
函数值属于引用类型,不可比较。因为函数不仅是一串代码,还记录了状态。函数值也叫闭包。

可变参数

在参数列表的最后一个参数类型之前加上省略符号“…”,这表示该函数会接收任意数量的该类型参数。
可变参数常用于格式化字符串。
看起来像切片类型参数,实际不是:

func f(...int) {}
func g([]int) {}
fmt.Printf("%T\n", f) // "func(...int)"
fmt.Printf("%T\n", g) // "func([]int)"

Deferred函数

在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当执行到该条语句时,函数和参数表达式得到计算,但直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。

defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。

这部分我感觉tour.go-zh.org描述的更清楚一些,其实就是defer语句的函数

方法

一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些方法,而一个方法则是一个一个和特殊类型关联的函数。一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。

声明

函数声明时在函数名前放一个变量,就相当于为这个变量对应的类型定义了一个方法。
这个变量就叫做接收器(receiver)。其他语言用this或者self作为接收器。一般建议使用类型的第一个字母。func (p Point) Distance(q Point) float64 {}
对象的方法调用,叫做选择器。p.Distance
只要底层类型不是指针或者接口,就可以定义方法。

基于指针的方法

一般约定假如一个类型有一个指针作为接收器的方法,那么该类型的所有方法都必须有一个指针接收器。
建议使用方式:

r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // "{2, 4}"
  • 接收器的实际参数和形式参数是相同的类型,都是类型T或者指针*T
  • 接收器实参是T,形参是*T,编译器会隐式取变量地址
  • 接收器实参是*T,形参是T,编译器会隐式解引用指针
    说人话:
    不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
func (p *Point) ScaleBy(factor float64) {}
func Distance(p, q Point) float64 {}

p := Point{1, 2}
p.ScaleBy(2) // 隐式取地址

pptr := &p
pptr.Distance(q) // 隐式解引用

嵌入结构体扩展类型

类似结构体的匿名成员,通过把匿名的类型嵌入到结构体,来把这个类型的方法传递给结构体,就可以在方法不重名的情况下直接调用匿名类型的方法。

方法表达式

需要把第一个参数传入作为接收器

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

distance := Point.Distance   // method expression
fmt.Println(distance(p, q))  // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"

scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p)            // "{2 4}"
fmt.Printf("%T\n", scale) // "func(*Point, float64)"

// 译注:这个Distance实际上是指定了Point对象为接收器的一个方法func (p Point) Distance(),
// 但通过Point.Distance得到的函数需要比实际的Distance方法多一个参数,
// 即其需要用第一个额外参数指定接收器,后面排列Distance方法的参数。
// 看起来本书中函数和方法的区别是指有没有接收器,而不像其他语言那样是指有没有返回值。

当需要根据一个变量来决定调用哪个函数时,方法表达式很有用。

type Point struct{ X, Y float64 }

func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }

type Path []Point

func (path Path) TranslateBy(offset Point, add bool) {
    var op func(p, q Point) Point
    if add {
        op = Point.Add
    } else {
        op = Point.Sub
    }
    for i := range path {
        // Call either path[i].Add(offset) or path[i].Sub(offset).
        path[i] = op(path[i], offset)
    }
}

封装

一个对象的变量或者方法如果对调用方是不可见的。有时也叫做信息隐藏。
封装一个对象必须将其定义为struct。
优点

  • 因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并且只要弄懂少量变量的可能的值即可。
  • 隐藏实现的细节,可以防止调用方依赖那些可能变化的具体实现,这样使设计包的程序员在不破坏对外的api情况下能得到更大的自由。
  • 阻止了外部调用方对对象内部的值任意地进行修改。因为对象内部变量只可以被同一个包内的函数修改,所以包的作者可以让这些函数确保对象内部的一些值的不变性。

接口

接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。
一种抽象类型,不会暴露出所代表的对象的内部值的结构和这个对象支持的基础操作集合;只会表现出它们自己的方法。当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。
约定:

  • 调用者需要提供具体类型的值
  • 接口约定了类型需要满足的方法

接口类型

接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。
通过组合已有接口来定义新的接口类型:

package io
type Reader interface {
    Read(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}
type ReadWriter interface {
    Reader
    Writer
}

实现接口的条件

一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。
io.Writer这个接口需要Write方法,os.File和bytes.Buffer都有,但是time.Duration没有

var w io.Writer
w = os.Stdout           // OK: *os.File has Write method
w = new(bytes.Buffer)   // OK: *bytes.Buffer has Write method
w = time.Second         // compile error: time.Duration lacks Write method

断言一个值是否实现了接口类型:
var _ io.Writer = (*bytes.Buffer)(nil)

 类似资料: