Golang 核心36讲笔记(一)

3个环境变量

  • GOROOT、GOPATH、GOBIN
  • 环境变量GOPATH的值可以是一个目录的路径,也可以包含多个目录路径,每个目录都代表GO语言的一个工作区(workspace)。这些工作区用于放置Go语言的源码文件(source file),以及安装后的归档文件(archinvefile 也就是以.a为扩展名的文件)和可执行文件

Go语言源码的组织方式

  • Go语言的源码是以代码包为基本组织单位的
  • 一个代码包中可以包含多个以.go为扩展名的源码文件,这些源码文件都需要被声明为属于同一个代码包。代码包的名称一般会与这些源码文件所在的目录同名,如果不同名那么在构建、安装的过程中会以代码包名称为准
  • 导入代码包 import "github.com/labstack/echo",在工作区中一个代码包导入的路径实际上就是从src子目录,到该包的实际存储位置的相对路径

源码安装后的情况

  • go源码文件通常会被放在某个工作区的src子目录下
  • 安装后产生了归档文件,就会被放进该工作区的pkg子目录;
  • 如果产生了可执行文件,就可能会放进该工作区的bin子目录。
  • 小结:源码文件会以代码包的形式组织起来,一个代码包其实就是对应一个目录。安装某个代码包而产生的归档文件是与这个代码包同名的
  • 执行命令go install github.com/labstack/echo
  • 生成的归档文件相对目录就是github.com/labstack,文件名为echo.a
  • 注意:归档文件的箱规目录与pkg的目录之间还有一级目录,叫做平台相关目录(由操作系统_目标计算架构代号如:linux_amd64)

    构建和安装的GO的过程

  • 构建命令:go build;安装命令go install;
  • 构建和安装代码包都会执行编译、打包等操作,
  • 构建和安装操作生成的任何文件都会被先保存到某个临时的目录中。
  • 如果构建的是库源码文件,操作的结果文件只会存在于临时目录中,这里的构建的主要意义在于检查和验证
  • 如果构建的是命令源码文件,那么操作的结果文件会被搬运到那个源码文件所在的目录中
  • 安装操作会先执行构建,然后还会进行连接操作,`且把结果文件搬运到指定目录。进一步说,如果安装的是库源码文件,那么结果文件会被搬运到它所在的工作区的pkg目录下的某个子目录中
  • 安装的是命令源码文件,那么结果文件会被搬运到它所在的工作区的bin目录中,或者环境变量GOBIN指向的目录中。
  • 小结:构建和安装的不同之处,以及执行相应命令后得到的结果文件都会出现在哪里。

go build 命令

  • go build 默认不会编译目标代码包所依赖的那些代码包,当然如果被依赖的代码包的归档文件不存在,或者源码文件有了变化,那它还是会被编译。如果需要强制编译 使用 go build -a
  • go build -x 这样可以看到go build 命令都执行了哪些操作;使用 -n 可以只查看具体操作而不执行他们。
  • go build -v 这样可以看到go build 命令编译的代码包的名称。它在与-a 标记搭配使用时很有用
  • go get
  • 命令go get会自动从一些主流公用的代码仓库下载目标代码包,并把他们安装到环境变量GOPATH包含的第1工作区的相应目录中。
  • 常用标记
  • go get -u:下载并安装代码包,不论工作区中是否已经存在它们
  • go get -d:只下载代码包不安装
  • go get -fix:下载代码包之后先运行一个用户根据当前go版本修正代码的工具,然后再安装代码包
  • go get -t :同时下载测试所需的代码包
  • go get -insecure: 允许通过非安全的网络协议下载和安装代码包
  • github上的三方工具:glide,gb;官方的dep、vgo

GO命令源码文件

  • 命令源码文件
  • 库源码文件
  • 测试源码文件
  • 命令源码文件是程序的运行入口,是每个可独立运行的程序必须拥有的。我们可以通过构建或者安装,生成与其对应的可执行文件,后者一般会与该命令源码文件的直接父目录同名。
  • 如果以个源码文件声明属于main包,并且包含一个无参数声明且无结果声明的main函数,那么它就是命令源码文件:
  • package main
  • import "fmt"
  • func main() {
    fmt.Println("Hello,world!")
    }

命令源码文件如何接收参数

  • GO语言标准库有一个代码包专门用于接收和解析命令参数:flag包。
  • flag.StringVar(&name, "name", "everyone", "The greeting object.")
  • &name: 存储该命令参数的值的地址
  • name:参数的名称
  • everyone:默认值
  • 第4个参数是命令参数的简短说明
  • 使用:go run demo.go -name="Robert"

库源码文件

库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用。
tips:程序实体在Go语言中是指变量、常亮、函数、结构体和接口的统称。程序实体的名字被统称为标识符。

  • 这里讲的就是在一个go源码文件中怎么样使用别的文件里面的方法
  • 1 把文件放到同一个文件夹下,声明同样名字的包
  • 2 函数首字母大写

go语言类型推断

  • 重构:不改变某个程序的与外界的任何交互方式和规则,而只改变其内部实现的代码修改方式,叫做对该程序的重构。

  • GO语言类型推断可以明显提升程序的灵活性,使得代码重构变得更加容易,同时又不会给代码的维护带来额外负担,更不会损失程序的运行效率

  • 变量重声明:
    对已经声明过的变量再次声明。

  • 再次声明时赋予的类型必须与其原本的类型相同

  • 只看发生在某一个代码块中

  • 使用短变量声明时才会发生【name := "jack"】

  • 被声明并赋值的变量必须是多个,并且至少一个新的变量。这时我们才可以说对其中的旧变量进行了重声明。

  • var err error

  • n,err := io.WriteString(os.Stdout, "Hello,everyone!\n")
    我使用短变量声明对新变量n和旧变量err进行“声明并赋值”,这时也是对后者的重声明。

    判断变量的类型

    答:类型断言表达式
    类型断言表达式的语法形式是x.(T)。其中x代表的是要被判断类型的那个值。

  • value,ok := interface{}(container).([]string)
    interface{} 达标了不包含任何方法定义的、空的接口类型
    []string 是一个类型字面量:用来表示数据类型本身的若干个字符

  • 等号的右边就是类型断言表达式。

  • value和ok 分别代表表达式的返回值,ok是布尔类型的,代表类型判断的结果,true或者false
    如果是true,那么被判断的值将会被自动转换为[]string类型的值,并赋值给变量value,否则value将会赋予为nil。

  • x被称为源值,它的类型就是源类型,而那个T代表的类型就是目标类型。

  • 类型转换注意

    • 对于整数类型值、整数常量之间的类型转换,原则上只要源值在目标类型的可以表示范围内就是合法的
    • 特别注意,源类型范围大于目标类型范围的时候,如
    • var srcInt = int16(-255)
    • dstInt := int8(srcInt)
    • dstInt的值为1
    • 解析:整数在go语言是以补码的形式存储的,主要是为了简化计算机对整数的运算过程。补码其实就是原码各位求反再加1。
    • int16类型的-255的补码是1111111100000001转为int8类型,那么Go语言会把在较高位置上的8位二进制数直接截掉,从而得到00000001,由于最左边符号位是0,表示它是个正整数,正整数的补码等于它的源码所以结果dstInt的值是1;
    • 知识点:整数值类型的有效范围由宽变窄,只需要在补码形式下截掉一定数量的高位二进制即可; 浮点数类型的值转为整数类型值时,前者的小数部分会被全部截掉
    • 知识点2:把整数转为以个string类型的值是可行的,被转换整数值应该可以代表一个有效的Unicode代码点,否则转换的结果将会乱码(菱形当中有个问号);这个Unicode代码点的U+FFFD是Unicode标准中定义的Replacement Character 专门用于替换哪些未知的,不被认可的以及无法展示的字符。

数组和切片

  • array:值长度是固定的,值类型
  • slice:值是可变长的,引用类型【通道,字典,函数】
  • 传引用:传递的值是引用类型
  • 传值:传递的值是值类型
  • 传递成本上引用类型的值往往要比值类型的值低很多
  • len()和cap() 分别可以获取到长度和容量
  • 数组的容量永远都等于长度,而切片的容量却不是这样。
  • 举例:
  • s3 := []int{1,2,3,4,5,6,7,8}
  • s4 := s3[3:6]
  • s3的容量是和长度都是8;s3[3:6]代表从索引为3的位置开始截取直到索引为6-1=5的位置,那么s4的长度为3(6-3),但是它的cap是5,简单来说是8-3(起始索引)=5
  • 怎么正确估算切片的长度和容量?
  • 一旦切片容纳不了更多的元素,go就会扩容,它不是改变原来的切片,而是会生成一个容量更大的切片,然后将原来的元素和新元素一并拷贝到新切片中。
  • 一般新切片是原来的切片容量的2倍;当原切片的长度大于等于1024时,GO会将原容量的1.25倍作为新容量的基准(不断的*1.25直到能容纳下所有的元素),如果这个值超过了原切片长度的2倍,那么以新长度为基准

链表container/list

这个包包含了两个公开的程序实体:List和Element
前者实现了一个双向链表,后者代表了链表中元素的结构。

  • 链表的元素不是连续存储的,所以相邻的元素之间需要互相保存对方的指针,每个元素还要保存它所属那个链表的指针
  • List有MoveBefore和MovaAfter方法,把给定元素移动到另一个元素的前面和后面。
  • MoveToFront和MoveAfter,给定元素移动到链表的最前面或者最后面
  • Front()和Back()可以获取最前和最后的元素
  • InsertBefore()和InsertAfter()在指定的元素之前和之后插入新元素
  • PushFront()和PushBack()方法则用于最前端和最后端插入新元素

map 字典

  • 字典类型是哈希表的特定实现,键和元素的最大不同在于,前者类型是受限的,而后者却可以是任意类型的。
  • 字典键类型不能是函数类型、字典类型和切片类型
    因为go语言规范规定,在键类型的值之间必须可以试驾操作符===和!= 以上3种类型不支持判等等操作
    这个具体之前的blog有讲。
  • 这里说一点就是键的哈希值相等的时候,会跟键再次比较一下。

通道和goroutine

Don't communicate by sharing memory; share memory by communicating

  • 通道可以用来在多个goroutine之间传递数据
  • 通道类型的值本身就是并发安全的
  • ch1 := make(chan int,3)
  • 3代表通道的容量,所谓容量就是指通道最多可以缓存多少个元素值。这个3是int型,但是不能小于0
  • 容量为0的通道称之为非缓冲通道
  • 一个通道相当于一个先进先出的队列,各个元素严格的按照发送的顺序排列
  • <- 这个符号叫接送操作符
  • 通道接收和发送的基本特性
    • 对于同一个通道,发送操作之间是互斥的,接收操作之间是也互斥
    • 发送操作和接收操作中对元素值的处理都是不可分割的
    • 发送操作在完全完成之前会被阻塞,接收操作也是如此
    • 针对缓冲通道的情况,如果通道已满,对它的所有发送操作都会被阻塞,直到通道中有元素值被接走。这时通道会优先通知最早因此而等待的,那个发送操作所在的goroutine,它会再次执行发送操作;相对的接收操作也这样,如果通道空了,那么它的所有接收操作都会被阻塞,直到有新元素出现,通道会通知最早的goroutine,并使它再次执行接收操作。
    • 数据直接从发送方复制到接收方,中间并不会用费缓冲通道做中转。相比之下,缓冲通道则是在用异步的方式传递数据。注意,如果发送操作执行的时候发现空的通道中,正好有等待的接收操作,那么它就会直接把元素值复制给接收方
  • 通道是一个引用类型
  • 通道关闭之后再进行发送操作会引发panic
  • 试图关闭一个已经关闭了的通道会panic

    通道的高级玩法***

  • 单向通道:只能发不能收或者只能收不能发的通道
    • 只发不收(发送通道):var uselessChan = make(chan<- int,1)
    • 只收不发(接收通道):var uselessChan = make(<-chan int,1)
    • 单向通道的应用价值在于约束其他的代码行为
      • type Notifier interface {
        SendInt(ch chan<- int)
        }
      • 一个类型如果想成为一个接口类型的实现类型,那么就必须实现这个借口中定义的所有方法。(类似php的interface所有继承接口的类必须实现这个接口的所有方法)因此,如果我们在某个方法的定义中使用了单向通道类型,那么就相当于在对它的所有实现做出约束。在合理Notifier接口中的SendInt方法只会接受一个发送通道作为参数,如果传递了元素类型匹配的双向通道,GOlang会自动把双向通道转换为函数所需的单向通道。
  • 通道取值
  • for elem := range intChan2 {}
    • 不断的从intChan2中取出元素值,即使intChan2被关闭,也会再取出所有剩余元素值之后再结束执行
    • 当intChan2中没有元素值时,它会被阻塞在for关键字那一行,知道有新元素可取
    • 假设intChan2的值为nil 那么它会被永远阻塞在有for关键字的那一行
  • select 语句
    • 分支有2种:候选分支,静默分支
    • 候选分支:case开头+表达式+冒号
    • 静默分支:default+冒号
    • select {
      case <-intChan2:
      case elem :=<-intChan2:
      default:
      }
    • 静默分支,不论涉及通道操作的表达式是否阻塞,select 语句都不会被阻塞,如果那几个表达式都阻塞了,或者没有满足求值条件,那么静默分支就会被选中执行
    • 如没有静默分支,所有的case都不满足的情况下,select语句会被阻塞直到至少一个case表达式满足条件
    • select 语句只能对其中的每一个case表达式各求职一次。
    • 分支选择规则
    • case表达式中有表达式的时候,从左到右的顺序被求值
    • case表达式中在该语句执行开始时先被求值
    • case表达式操作处于阻塞状态,那么对case表达式求值就是不成功,这样是不满足选择条件的
    • 仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。
    • select 语句发现多个候选分支满足选择条件时,那么它会用一种伪随机的算法在这些分支中选择一个并执行。
    • select 只能有一个default,无论位置写在哪都是在无候选分支时候才被执行
    • select语句的每次执行,包括case 表达式求值和分支选择,都是独立。不过至于它的执行是否并发安全,就要看其中的case表达式以及分支中,是否包含并发不安全的代码了。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注