Skip to content

Instantly share code, notes, and snippets.

@BruceChen7
Last active August 23, 2020 15:44
Show Gist options
  • Save BruceChen7/c6594f91a197b9b19484e2e5e6ac83b8 to your computer and use it in GitHub Desktop.
Save BruceChen7/c6594f91a197b9b19484e2e5e6ac83b8 to your computer and use it in GitHub Desktop.
#golang#reflection#effective#slice#embedding

目录

资料

nil

各个类型默认值

bool      -> false                              
numbers -> 0                                 
string    -> ""      

pointers -> nil
slices -> nil
maps -> nil
channels -> nil
functions -> nil
interfaces -> nil


type Person struct {
  AgeYears int
  Name string
  Friends []Person
}

var p Person // Person{0, "", nil}


var p *int
p == nil    // true
*p          // panic: invalid memory address or nil pointer dereference

slice 与nil

// nil slices
var s []slice
len(s)  // 0
cap(s)  // 0
for range s  // iterates zero times
s[i]  // panic: index out of range

map与nil

// nil maps
var m map[t]u
len(m)  // 0
for range m // iterates zero times
v, ok := m[i] // zero(u), false
m[i] = x // panic: assignment to entry in nil map

对于nil的map,我们可以简单把它看成是一个只读的map,不能进行写

func NewGet(url string, headers map[string]string) (*http.Request, error) {
  req, err := http.NewRequest(http.MethodGet, url, nil)
  if err != nil {
    return nil, err
  }

  for k, v := range headers {
    req.Header.Set(k, v)
  }
  return req, nil
}


NewGet("http://google.com", map[string]string{
  "USER_AGENT": "golang/gopher",
},)

// 可以这样传入一个nil
NewGet("http://google.com", map[string]string{})
// 也可以这样
NewGet("http://google.com", nil)

channel与nil

// nil channels
var c chan t
<- c      // blocks forever
c <- x    // blocks forever
close(c)  // panic: close of nil channel

// 假如现在有两个channel负责输入,一个channel负责汇总,简单的实现代码:
func merge(out chan<- int, a, b <-chan int) {
  for {
    select {
      case v := <-a:
        out <- v
      case v := <- b:
        out <- v
    }
  }
}
// 如果在外部调用中关闭了a或者b,那么就会不断地从a或者b中读出0,这和我们想要的不一样,我们想关闭a和b后就停止汇总了,
// 修改一下代码:
func merge(out chan<- int, a, b <-chan int) {
  for a != nil || b != nil {
    select {
      case v, ok := <-a:
          if !ok {
            a = nil
            fmt.Println("a is nil")
            continue
          }
          out <- v
      case v, ok := <-b:
          if !ok {
            b = nil
            fmt.Println("b is nil")
            continue
          }
          out <- v
    }
  }
  fmt.Println("close out")
  close(out)
}

在知道channel关闭后,将channel的值设为nil,这样子就相当于将这个select case子句停用了,因为nil的channel是永远阻塞的。

nil与interface

interface并不是一个指针,它的底层实现由两部分组成,一个是类型,一个值,也就是类似于:(Type, Value)。只有当类型和值都是nil的时候,才等于nil。看看下面的代码:

func do() error {   // error(*doError, nil)
  var err *doError
  return err  // nil of type *doError
}

func main() {
  err := do()
  // 输出的是false
  fmt.Println(err == nil)
}

输出结果是false。do函数声明了一个*doErro的变量err,然后返回,返回值是error接口,但是这个时候的Type已经变成了 (*doError,nil),所以和nil肯定是不会相等的。所以我们在写函数的时候,不要声明具体的error变量,而是应该直接返回nil:

func do() error {
  return nil
}

func do() *doError {  // nil of type *doError
  return nil
}

func wrapDo() error { // error (*doError, nil)
  return do()       // nil of type *doError
}

func main() {
  err := wrapDo()   // error  (*doError, nil)
  fmt.Println(err == nil) // false
}

这里最终的输出结果也是false。为什么呢?尽管wrapDo函数返回的是error类型,但是do返回的却是*doError类型,也就是变成了(*doError,nil),自然也就和nil不相等了。因此,不要返回具体的错误类型。遵从这两条建议,才可以放心地使用if x != nil。

defer

What

A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

how

//demo1 defer使用实例
func main() {
    fmt.Println("test1")
    defer fmt.Println("defer")
    fmt.Println("test2")
}

// 输出
// test1
// test2
// defer

使用规则

Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed.

  • 一个函数中有多个defer时的运行顺序
  • defer语句执行时的拷贝机制
  • defer如何影响函数返回值
//demo3 多个defer执行顺序
func main() {
    fmt.Println("test1")
    defer fmt.Println("defer1")
    fmt.Println("test2")
    defer fmt.Println("defer2")
    fmt.Println("test3")
}
// test1
// test2
// test3
// defer2
// defer1


//demo4 defer函数在defer语句执行那一刻就已经确定
// I am function test1
func main() {
    test := func() {
        fmt.Println("I am function test1")
    }
    defer test()
    test = func() {
        fmt.Println("I am function test2")
    }
}


//demo5 defer函数参数的值在注册那一刻就已经确定
// 打印10
func f5() {
    x := 10
    defer func(a int) {
        fmt.Println(a)
    }(x)
    x++
}


// demo6 defer 函数传递参数为指针传递
// 11
func main() {
    x := 10
    defer func(a *int) {
        fmt.Println(*a)
    }(&x)
    x++
}

//demo7 defer 延迟函数为闭包
// 闭包里的变量本质上是对上层变量的引用
// 11
func main() {
    x := 10
    defer func() {
        fmt.Println(x)
    }()
    x++
}


// demo10 defer函数与非命名返回值之间的关系
// 返回10
func f10() int {
    x := 10
    defer func() {
        x++
    }()
    return x
}

//demo11 defer函数与非命名返回值之间的关系
// 返回11
func f11() *int {
    a := 10
    b := &a
    defer func() {
        *b++
    }()
    return b
}
func main() {
    fmt.Println("f10", f10())
    fmt.Println("f11", *f11())
}


//拆解 demo10_1 defer函数与非命名返回值之间的关系, return拆解
func f10_1() int {
    x := 10
    defer func() {
        x++
    }()
    //return x => 拆解
    _result := x
    return _result //实际返回的是_result的值,因此defer中修改x的值对返回值没有影响
}

//demo11_1 defer函数与返回值之间的关系, return拆解
func f11_1() *int {
    a := 10
    b := &a
    defer func() {
        *b++
    }()
    //return b => 拆解
    _result := b
    return _result //执行defer函数调用*b++, 修改了b指向的内存空间的值,实际返回的是result指针
}

//demo13 defer函数与命名返回值之间的关系
// 11
func f13() (result int) {
    defer func() {
        result++
    }()
    return 10
}

// demo14 defer函数与命名返回值之间的关系
// 11
func f14() (result int) {
    result = 10
    defer func() {
        result++
    }()
    return result
}

//demo15 defer函数与命名返回值之间的关系
// 12
func f15() (b *int) {
    a := 10
    b = &a
    fmt.Println("b", b)
    defer func() {
        c := 12
        b = &c
        fmt.Println("defer", b)
    }()
    return
}

func main() {
    fmt.Println("f13", f13())
    fmt.Println("f14", f14())
    t := f15()
    fmt.Println("f15", *t, t)
}

defer 与recovery

//demo16 defer与recover
// catch error: TEST
// I am ok
func f16() {
    defer func() {
    	// 捕获异常
        if err := recover(); err != nil {
            fmt.Println("catch err:", err)
        }
    }()
    panic(errors.New("TEST"))
}

func main() {
    f16()
    fmt.Println("I am OK.")
}

why

清理资源,避免遗忘

f, err := os.Open(filename)
    if err != nil {
        panic(err)
    }
    defer f.Close()  //释放资源
    /*
        读取和处理文件内容
    */

effective

格式化

  • 格式化,使用gofmt(go fmt命令):
  • 缩进使用tab而非空格。
  • 行长度不限,可以自己缩进换行。
  • 括号用的比较少: if, for, switch。

注释文档(godoc/go doc)

  • 包(package)注释在文档的最上方。
  • 函数文档在函数头部(函数首字母大写才能被导出)。

Name

  • 包(package)名:小写,简洁,单词,无下划线。io.Reader/ring.New
  • Getters/Setters:obj.Owner()/obj.SetOwner(x)
  • 函数/变量:首字母大写才可导出(export)被外部模块使用。
  • 驼峰命名法:不使用下划线,如MixedCaps/mixedCaps。

defer

  • 函数退出时调用对应的call。
  • LIFO。
  • 参数是在执行defer语句时确定的,而非对应函数执行时。

分配对象

  • new:返回指针,所有内容为零值。
  • make:返回对象,仅适用于slices, maps, channels。

Array

  • 值对象:赋值是拷贝所有元素。
  • 参数传递是值传递,即函数操作的是一份拷贝,不会影响原有对象。
  • 长度是类型的一部分,[10]int和[20]int不是同一个类型。

Slices

  • 引用对象:赋值其实指向的同一个array。
  • 尽量使用slices替代array。

Maps

  • 引用对象。
  • v := map[key] 不存在的key会赋零值。
  • value, ok := m[key] 不存在的key ok为false。 删除:delete(m, key)。key不存在时也是安全的。

Printing

  • 带f的都有格式化参数,不带的按照类型默认的输出格式输出。
  • 自定义类型%v格式化输出需要实现String()方法。
  • 实现String()方法尽量防止%s格式的隐形转化,可能会死循环。
  • 可变参数:func foo(args …T)。
  • 切片打散:append(slice1, slice2…)。

常量(Constants):

  • 编译时确定。
  • 右值如果是表达式,那么因子都必须是常量。
  • iota的使用。

init函数

  • 每个文件都可以含有若干个init函数,会在第一次引用的时候执行,且只执行一次。
  • 一个文件如果有多个init函数,则按上下顺序执行。
  • 一个package里的多个文件会按文件名的字典顺序执行,

值(values)和指针(pointers)

  • 值方法(method)可以被值类型或者指针类型调用。
  • 指针方法只能被指针类型调用。
  • 指针方法能改变结构的内容,值方法不行。
  • 优先声明methods on *T unless you have a strong reason to do otherwise.

接口(interfaces)

  • 含义:如果X可以做这件事(实现了这些方法),那么X就能在这用。
  • 几乎所有类型都可以增加方法,也即可以实现接口,如:内置类型,通道,函数等。
  • 原则:如果一个类型设计上仅需要实现一个接口,那么不应该暴露自己的类型给外部,而是暴露接口的类型(举例:hash算法很多,不同的hash算法类型构造器应返回接口类型,这样替换hash算法时只需要修改一处构造代码)。

类型转换

  • 获得值的类型:typeName := value.(type),常用语switch语句。
  • 类型转换:v, ok := value.(typeName)。没有ok参数失败会出现运行时错误。

嵌入

  • struct/interface都可以使用。
  • 不带有变量名,仅含有类型:匿名成员?。
  • 初始化:和正常一样初始化。T{v1, v2, v3…}。
  • 引用:直接操作类型(job.Logger.Printf,其中Logger是类型)。
  • 好处:和继承类似,上层类型可以直接调用下层类型的方法。
  • 坏处:名字冲突:上层的名字优先级高于下层。

并发

  • 不要通过共享内存来交互,通过交互来共享内存。
  • 尽量使用channel通信。

Goroutines

  • 多个协程共享同一地址空间。
  • 轻量级的,比栈消耗资源多点。
  • 有限使用栈,栈内存会随需要增大,如果有需要则会使用堆。
  • 协程+多线程(具体调度较复杂)。

Channels

  • make关键字创建。
  • 引用类型。
  • 第二个整形参数代表bufferk大小,默认0。ci := make(chan int) cj := make(chan int, 3)。
  • 用途:a) 消息队列。 b) 协程间数据的同步交互。 c) 并发控制。

并行(Parallelization)

  • CPU核心数:runtime.NumCPU()
  • go能使用的最大核心数:runtime.GOMAXPROCS,默认值为runtime.NumCPU。

Error & Panic

  • Error:可恢复错误,实现Error接口。
  • Panic:不可恢复错误。一般初始化失败的时候使用。
  • Panic发生时会根据调用栈一层层退出,但会执行defer调用。

Recover

  • 多协程时,一个协程出现panic异常退出希望不会影响到其他的协程。
  • panic可以被捕获,防止整个进程退出。
  • panic发生时会执行defer,所以只能在defer中去捕获panic。
  • 捕获panic仅仅在defer代码中调用recover()语句即可。
  • 在非defer语句中调用recover()会返回nil。
  • defer可以修改命名返回值,所以可以在defer中重制命名返回值。

interface

why interface

  • writing generic algorithm
  • hiding implementation detail
  • providing interception points
// 实现泛型编程,第一个优点
func sayHello(ig IGreeting) {
    ig.sayHello()
}

golang := Go{}
php := PHP{}

// golang与php实现了IGreeting接口,可以直接使用sayHello函数,即"泛型编程"
sayHello(golang)
sayHello(php)


// 通过返回接口,来隐藏实现细节
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)    //返回 cancelCtx
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) //返回 timerCtx
func WithValue(parent Context, key, val interface{}) Context    //返回 valueCtx

// 通过 interface,我们可以通过类似装饰器方式实现 dynamic dispatch
type header struct {
    rt  http.RoundTripper
    v   map[string]string
}

func (h header) RoundTrip(r *http.Request) *http.Response {
    for k, v := range h.v {
        r.Header.Set(k,v)
    }
    return h.rt.RoundTrip(r)
}

how interface

  • 接口嵌套
    • 接口类型间可以组合使用
    • 接口类型间的嵌入不会涉及方法间的”屏蔽”,只要方法同名就会产生冲突,并无法通过编译
  type Animal interface {
       // ScientificName 用于获取动物的学名。
       ScientificName() string
       // Category 用于获取动物的基本分类。
       Category() string
   }

   type Named interface {
       // Name 用于获取名字。
       Name() string
   }

   //包含Animal接口与Named接口
   type Pet interface {
       Animal
       Named
   }
  • 空接口
    • 空接口就是不包含任何方法的接口。因此,所有的类型都实现了空接口
    • []T不能直接赋值给[]interface{}
// 所有类型都实现了空接口
var v1 interface{} = 1
var v2 interface{} = "abc"
var v3 interface{} = struct{ X int }{1}

// 如果函数打算接收任何数据类型,则可以将参数声明为interface{}。最典型的例子就是标准库fmt包中的Print和Fprint系列的函数:
func Fprint(w io.Writer, a ...interface{}) (n int, err error)

// 下述代码在编译时会报错:cannot use t (type []int) as type []interface {} in assignment
t := []int{1, 2, 3, 4}
var s []interface{} = t
  • 区分接口存储类型的方法
    • 将函数里interface{}类型的参数转换为具体的类型的方法有两种:type assert和type switch判断

type assert

type assert: <目标类型的值>,<布尔参数> := <表达式>.( 目标类型 )

Params := make([]interface{}, 3)
Params[0] = 88                   // 整型
Params[1] = "go interface type"         // 字符串
Params[2] = Go{Name: "go"} // 自定义结构体类型

for index, v := range Params {
    if value, ok := v.(int); ok {
	fmt.Printf("[comma_ok]Params[%d] 是int类型 ,值:%v\n", index, value)
    }else if value, ok := v.(string); ok{
	fmt.Printf("[comma_ok]Params[%d] 是string类型 ,值:%v\n", index, value)
    }else if value, ok := v.(Go); ok{
	fmt.Printf("[comma_ok]Params[%d] 是自定义数据类型 ,值:%v\n", index, value)
    }
}

reflection

反射三原则:

  • Reflection goes from interface value to reflection object.
  • Reflection goes from reflection object to interface value.
  • To modify a reflection object, the value must be settable.

关键概念:

  • reflection object: 最关键的是reflection.Type与reflection.Value。也可以说是变量的元信息的类定义
  • interface value, 因为空接口 interface{}是可以作为一切类型值的通用类型使用。这里的接口值interface value可以理解为空接口变量值interface{} value

应用场景

  • 判断未知对象是否实现了具体接口

通常判断未知对象是否实现具体接口很简单,直接通过 变量名.(接口名) 类型验证的方式就可以判断。但也有例外,框架代码实现中检查调用代码的情况。因为框架代码先实现,调用代码后实现,也就无法在框架代码中通过简单额类型验证的方式进行验证

grpcServer := grpc.NewServer()
// 服务端实现注册
pb.RegisterRouteGuideServer(grpcServer, &routeGuideServer{})

注册的实现没有实现所有的服务接口时,程序就会报错。它是如何做的,我们用简单的场景来看下原理。

//目标接口定义
type Foo interface {
	Bar(int)
}
  
dst := (*Foo)(nil)
dstType := reflect.TypeOf(dst).Elem()

//验证未知变量 src 是否实现 Foo 目标接口
srcType := reflect.TypeOf(src)
if !srcType.Implements(dstType) {
		log.Fatalf("type %v that does not satisfy %v", srcType, dstType)
}

这段代码通常会是在程序的启动阶段所以对于程序的性能而言没有任何影响。

  • 结构体字段属性标签

通常定义一个待JSON解析的结构体时,会对结构体中具体的字段属性进行tag标签设置,通过tag的辅助信息对应具体JSON字符串对应的字段名,这里利用结构体字段属性标签做反射的例子

type App struct{
    // attributes ...
    // metrics ...
    metrics struct{
            // Size of the current server queue
    		QueueSize metrics.Gauge `metric:"thrift.udp.server.queue_size"`
    
    		// Size (in bytes) of packets received by server
    		PacketSize metrics.Gauge `metric:"thrift.udp.server.packet_size"`
    
    		// Number of packets dropped by server
    		PacketsDropped metrics.Counter `metric:"thrift.udp.server.packets.dropped"`
    
    		// Number of packets processed by server
    		PacketsProcessed metrics.Counter `metric:"thrift.udp.server.packets.processed"`
    
    		// Number of malformed packets the server received
    		ReadError metrics.Counter `metric:"thrift.udp.server.read.errors"`
    }
}

在应用中首先直接定义匿名结构metrics, 将针对该应用的metric探测标量定义到具体的结构体字段中,并通过其字段标签tag的方式设置名称。这样在代码的可读性大大增强了。初始化代码

import "github.com/jaegertracing/jaeger-lib/metrics/prometheus"
//初始化
metrics.Init(&app.metrics, prometheus.New(), nil)
  • 函数适配

http

func newRequest(method string, url *url.URL, body string) *http.Request {
	req, err := http.NewRequest(method, url.String(), createBody(body))
	if err != nil {
		log.Fatalf("unable to create request: %v", err)
	}
	for _, h := range httpHeaders {
		k, v := headerKeyValue(h)
		if strings.EqualFold(k, "host") {
			req.Host = v
			continue
		}
		req.Header.Add(k, v)
	}
	return req
}

tcp连接

func main() {
  server, err := net.Listen("tcp", ":6000")
    if err != nil {
      fmt.Println(err)
      os.Exit(1)
    }

    go func() { // Launch routine that will accept connections
      for {
        // 接受一个新的TCP连接
        conn, err := server.Accept()
        if err != nil {
          fmt.Println(err)
          os.Exit(1)
        }
        if len(users) < maxUsers {
          newConnection <- conn // Send to handle new user
        } else {
          // 向网络连接直接写入字符串
          io.WriteString(conn, "Server is full!")
        }
      }
    }()
}

资料来源

GOROOT 和GOPATH

GOROOT 指的是 Go 语言的执行路径也就是 Go 语言编译、工具、标准库等的安装路径,Go 一些常用的命令比如 go build ,go tool 等都依赖 GOROOT 里面的库来执行相应的命令,如果 go root 不配置则 Go 文件将无法被编译和运行.

# 查看GOROOT变量
go env GOROOT
 /usr/local/go

GOPATH

GOPATH表示 工作空间(Workspace),一般是 项目的开发路径,同时也是 GOPATH 是作为编译后二进制的存放目的地和 import 包时的搜索路 径,这个目录指定了需要从哪个地方寻找 Go 的包、可执行程序等。这个目录可以是多个目录,Go 编译或者运行时会从这个环境变量中去对应查找,当设置多个目录时需要使用分隔符区分(Linux 使用 : 冒号,Windows 使用 ; 分号)

按照约定工工作空间一般会包含以下三个目录:src 目录,pkg 目录,bin 目录

  • bin文件:都是通过 go install 命令,由 Go 源码文件编译生成的可执行文件
  • pkg 目录是用来存放通过 go install 命令安装后的代码包的归档文件(.a 文件)。
  • src 目录 以代码包的形式组织并保存 Go 源码文件,每个代码包都和 src 目录下的文件夹一一对应。每个子目录一般代表一个项目

go mod 的使用

基本使用

init 新开的项目,一般要使用go mod init来初始化,建立一个go.mod文件

go mod init

tidy

依赖的其他包可以用 tidy 命令生成。对于老项目,没有使用go modulue的,直接执行go mod init 初始化一个 go.mod 文件, 然后执行go mod tidygo.mod文件中添加依赖项

go mod tidy [-v]

这个命令会自动地根据你的源码去下载并分析各个用到的包,下载到的包会被缓存到 $GOPATH/pkg/mod/ 目录下,并且以 git tag、 git commit_id 等组织目录结构。还会在本地生成 go.sum 文件,需要注意的是,这个命令在帮你添加依赖的包外还会自动地移除本项目不再用到的包

download

go mod download [-json] [modules]

这个命令可以下载指定的依赖包到到缓存目录

vendor

go mod vendor [-v]

有时候我们的编译环境并没有外网时这个命令就比较有用了,他会根据go.mod 文件,从缓存中把依赖包复制到本项目的vendor目录下。需要注意的是,复制到vendor目录时,他会自动地不复制测试代码。-v 选项可以打印详细的信息到标准错误。

几个总结

  • go mod init 用来初始化一个module
  • go build, go test 在必要的时候添加依赖
  • go mod tidy 移除一些不必要的依赖
  • go get 用来更新相关模块

如何添加新依赖?

第一种方式: 代码中 import 新依赖后,执行 go mod tidy, tidy 会重新扫描整个项目代码并更新go.mod 文件, 但不好的地方是 tidy 不单单添加新依赖,还会自动更新其他老的依赖项,例如原先我们依赖A模块的0.0.1版本,而A模块远端已经有了0.0.2版本了,tidy 会自动地帮你把A模块更新为新版本。可以想到,我们原先的测试都是在0.0.1下测试的,那么到了0.0.2版本是否还兼容我们是不知道的,要重新测试。

第二种方式

在本项目目录下执行go get -u xxxxx, 这时 go get 会自动在go.mod文件中添加新依赖,老的依赖不会发生变化,所以更推荐用这个方法。

  • go get -u将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号)
  • go get -u=patch 将会升级到最新的修订版本
  • go get package@version 将会升级到指定的版本号version

golang官方推荐的最佳实践叫做semver,这是一个简称,写全了就是Semantic Versioning,也就是语义化版本。这里有语义化版本

语义化版本

版本的组成:vMAJOR.MINOR.PATCH.

  • Increment the MAJOR version when you make a backwards incompatible change to the public API of your module. This should only be done when absolutely necessary.
  • Increment the MINOR version when you make a backwards compatible change to the API, like changing dependencies or adding a new function, method, struct field, or type.
  • Increment the PATCH version after making minor changes that don't affect your module's public API or dependencies, like fixing a bug

几点注意事项

  • v0 版本不保证向后兼容,public API接口可能会改变,表示the initial, unstable version
  • v1 版本要保证在大版本内 public API接口不变,表示

发布版本

  • Run go mod tidy, which removes any dependencies the module might have accumulated that are no longer necessary.

  • Run go test ./... a final time to make sure everything is working.

  • Tag the project with a new version using git tag.

  • Push the new tag to the origin repository.

语义化版本带来的影响 如果你使用和发布的包没有版本tag或者处于1.x版本,那么你可能体会不到什么区别,因为go mod所支持的格式从始至终是遵循semver的,主要的区别体现在v2.0.0以及更高版本的包上。

如果旧软件包和新软件包具有相同的导入路径,则新软件包必须向后兼容旧软件包

资料

常用命令

  • go build, go test:下载并添加依赖到go.mod中。
  • go mod tidy:整理,增加缺失的包,移除没用的包。
  • go mod graph:show模块间的依赖图。
  • go mod why:show为什么需要包。
  • go list -m all:查看所有的依赖。
  • go get:下载依赖并更新到go.mod中

go get命令

  • @v0.3.2:指定tag,Release时建议此方法。
  • @master:master分支最新commit
  • @latest、不指定:默认行为,最新版本;有tag则最新tag,无tag则master分支最新commit。
  • version前使用>,>=,<,<=:大于/小于指定版本。
  • v0.0.3-pre:开发时版本较多时使用,不会被 @latest、不指定 自动拉取到。

几个概念

  • build list:构建列表,表示一个软件构建时所依赖的包版本列表
  • requirement:需求,表示一个软件所依赖的包
  • build list、requirement:后者表示一种需求,前者表示需求的一种具体形式

mod是采用的最小版本选择机制:即在不破坏功能的前提下,此系统中已不能再删除任何东西。 观察go mod升级/降级机制:

  • 创建当前build list。
  • 升级所有依赖到最新版本。
  • 升级单个依赖到指定版本。
  • 降级单个依赖到指定版本。

代码A的

package main
import (
    "fmt"
    _"github.com/xxx/B"
    _"github.com/xxx/C"
)
func main() {
    fmt.Println("main package")
}

go.mod

module github.com/xxx/A
go 1.12

不指定版本

执行go build命令时,会自动识别import中的路径,直接依赖的包自动选择最新版本,并递归地生成build list

指定依赖

不建议手动修改go.mod。指定A直接依赖的版本时,会递归地添加其依赖项,如果同一个依赖有两个不同版本时,会保留版本号最新的那个包。下面这个例子,同一个依赖D有两个不同的版本D1.3和D1.4,此时只会保留D1.4这个版本

module github.com/xxx/A
require (
    github.com/xxx/B v1.2
    github.com/xxx/C v1.3
)

升级所有module

升级所有module通过此命令:go get -u。可以升级所有直接和间接依赖。升级所有module时会先取新引入的module版本和已有的module版本的并集,再保留版本号最新的那个包。

升级一个module

升级一个module通过此命令:go get github.com/xxx/[email protected]。可以升级直接或间接依赖。 需要注意:对一个包的升级或降级会引入一连串的新版本的依赖;操作者需要明确知道自己在做什么操作再升级或降

@BruceChen7
Copy link
Author

BruceChen7 commented Apr 13, 2020

不指定A依赖的版本

image

指定A依赖的版本

image

@BruceChen7
Copy link
Author

导入包的过程

image

@BruceChen7
Copy link
Author

@BruceChen7
Copy link
Author

BruceChen7 commented Jul 29, 2020

slice

slice的结构

对于slice而言,实际上可以看成这么一种数据结构

type sliceHeader struct {
    Length        int
    Capacity      int
    ZerothElement *byte
}

也就是一个长度和一个指针,类似于vector这种数据结构。

func AddOneToEachElement(slice []byte) {
    for i := range slice {
        slice[i]++
    }
}

虽然slice是通过传值的方式传参,但是我们也同样改变了参数的slice,因为其传入复制的也是一个指针,所以修改是通过这个指针来进行的。

func SubtractOneFromLength(slice []byte) []byte {
    slice = slice[0 : len(slice)-1]
    return slice
}

func main() {
    fmt.Println("Before: len(slice) =", len(slice))
    newSlice := SubtractOneFromLength(slice)
    fmt.Println("After:  len(slice) =", len(slice))
    fmt.Println("After:  len(newSlice) =", len(newSlice))
}

对于下面代码而言,

slice := iBuffer[0:0]

可以理解成

slice := sliceHeader{
    Length:        0,
    Capacity:      10,
    ZerothElement: &iBuffer[0],
}
if cap(slice) == len(slice) {
    fmt.Println("slice is full!")
}

Capacity是最大容量。指定slice的容量

gophers := make([]Gopher, 10) // len为10,capacity也是10
gophers := make([]Gophter, 10, 15) // len 10, capacity 15


newSlice := make([]int, len(slice), 2*cap(slice))
copy(newSlice, slice)  // slice解决了overlap的问题

slice 与 nil

// nil实际上可以类比成
sliceHeader{
    Length:        0,
    Capacity:      0,
    ZerothElement: nil,
}  // or
sliceHeader {

}

上面的代码和下面是有区别的:

array[0:0]

这种方式虽然len为0,但是其指针并不是nil。

slice与array

  • 数组的拷贝是副本拷贝。对于副本的改变不会影响到原来的数组。
  • 但是,切片的拷贝很特殊,切片的拷贝只是对于运行时切片结构体的拷贝,切片的副本仍然指向了相同的数组。所以,对于副本的修改会影响到原来的切片
//数组是值类型
a := [4]int{1, 2, 3, 4}

//切片是引用类型
b := []int{100, 200, 300}

c := a
d := b

c[1] = 200
d[0] = 1
//output: c[1 200 3 4] a[1 2 3 4]
fmt.Println("a=", a, "c=", c)
//output: d[1 200 300]  b[1 200 300]
fmt.Println("b=", b, "d=", d)


numbers := make([]int, 0, 20)


//append一个元素
numbers = append(numbers, 0)

//append多个元素
numbers = append(numbers, 1, 2, 3, 4, 5, 6, 7)

//append添加切片
s1 := []int{100, 200, 300, 400, 500, 600, 700}
numbers = append(numbers, s1...)

//now:[0 1 2 3 4 5 6 7 100 200 300 400 500 600 700]


/  删除第一个元素
numbers = numbers[1:]

// 删除最后一个元素
numbers = numbers[:len(numbers)-1]

// 删除中间一个元素
a := int(len(numbers) / 2)
numbers = append(numbers[:a], numbers[a+1:]...)

应该注意的是,当你需要通过method,改变slice header的时候,那么应该传递slice的指针

func PtrSubtractOneFromLength(slicePtr *[]byte) {
    slice := *slicePtr
    *slicePtr = slice[0 : len(slice)-1]
}

func main() {
    fmt.Println("Before: len(slice) =", len(slice))
    PtrSubtractOneFromLength(&slice)
    fmt.Println("After:  len(slice) =", len(slice))
}

string

string可以看成是不可以变化的slice。因为其只能读,所以没有capacity这个概念,因为没有必要扩充。其他方面都可以看成a slice of bytes

slash := "/usr/ken"[0] // yields the byte value '/'.
str := string(slice)
slice := []byte(usr)

@BruceChen7
Copy link
Author

BruceChen7 commented Aug 18, 2020

embedding in go

type Base struct {
  b int
}

type Container struct {     // Container is the embedding struct
  Base                      // Base is the embedded struct
  c string
}
co := Container{}
co.b = 1   // 直接使用b
co.c = "string"
fmt.Printf("co -> {b: %v, c: %v}\n", co.b, co.c)

// 字面值常量
co := Container{Base: Base{b: 10}, c: "foo"}
fmt.Printf("co -> {b: %v, c: %v}\n", co.b, co.c)

// 定义Describe
func (base Base) Describe() string {
  return fmt.Sprintf("base %d belongs to us", base.b)
}
fmt.Println(cc.Describe()) // 子类的Describe,直接调用Base的

// 类似于直接调用
type Container struct {
  base Base
  c string
}

func (cont Container) Describe() string {
  return cont.base.Describe()
}

struct 中嵌入接口

type StatsConn struct {
  net.Conn

  BytesRead uint64
}
func (sc *StatsConn) Read(p []byte) (int, error) {
  n, err := sc.Conn.Read(p)
  sc.BytesRead += uint64(n)
  return n, err
}

直接在struct中嵌入接口后,需要在定义StatsConn中初始化的时候,进行赋值

conn, err := net.Dial("tcp", u.Host+":80")
if err != nil {
  log.Fatal(err)
}
sconn := &StatsConn{conn, 0}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment