[Golang学习]从零开始Go语言学习笔记(教材:go101)(长长文)

Go语言学习之旅

写在最前,由于笔记本坏了快两个月了,go语言的学习就停滞了。整理了一下以前硬盘的笔记,就都发出来吧,便于复习。
笔记都是之前闲暇时间从go101项目学习的,感觉go真的是一门很有意思的语言。

安装过程略过

官网说必看go effective(科网)


github有一个教程叫go101于是clone下来准备照着这个来学语法。

根据其README来启动go101,go run *.go


阅读开始

Chapter 1 About GO 101 这本书写来干嘛的

开篇作者以为自己掌握了Go,想要写一本教程书,然后发现自己一无所知哈哈哈,然后积累了几年,终于牛逼了。提醒学者不能轻敌,要有对的attitude。

提到了

  • go语言的api封装性挺好,官方解释的文档也很简单
  • go的优点有:
    • 静态语言但是有动态的灵活性
    • 内存节省,fast program warming-up?,执行的快 三大优点集一身不容易啊
    • 自带并行,可读性高,跨平台,内核稳定
  • 书本会详细介绍基本概念和术语,值的部分?,内存block部分,interface values?,

致谢略过不读

Chapter 2 An introducion of Go

  • 编译型静态强类型语言 compiled and static typed

  • go的features:

    • built-in concurrent自带并行编程支持
      • 叫goroutines的东西,green threads绿色线程?
      • channels隧道?是based on CSP模型的 TODO:去了解一下
    • 容器类型map和slice?一等公民?
    • 接口的多态?polymorphism
    • value boxing and reflcetion through interfaces?
    • 指针
    • 函数闭包
    • 方法
    • 类型嵌入(type embedding)?
    • 类型deduction
    • 内存安全
    • 自动垃圾回收
    • 跨平台兼容性
  • 一些高光features:

    • 语法设计简单,快速上手
    • 很多很棒的标准代码库,大多数都是跨平台的
    • go社区

    用go写代码的可以叫gophers哈哈

  • go的编译器,叫Go compiler或者gc(abbreviation),go team还有一个gccgo

    • gc 有官方的go sdk
    • For example, noticeable lags caused by garbage collecting is a common criticism for languages with automatic memory management. lag啥含义
    • 跨平台: Linux上编译的东西在window也能运行,反之亦然
    • 编译贼快
  • 执行go的优势:

    • 小内存占用

    • 快速代码执行

    • 短启动时间?short warm-up duration (so great deployment experience)

      下面是C/C++/Rust等编译型语言没有的

    • 快编译结果,本地开发快

    • 灵活动态语言

    • 内置concurrent编程支持!

  • 也有一些shortcomings:

    • 不支持泛型编程(C++)

The Official Go SDK

复习一下什么是sdk: 软件开发工具包 software development kit

第一次接触sdk,应该就是各种函数库吧

这一章讲怎么setup和运行简单programs

8过我已经安装好了

最简单的程序

package main

func main() {
    
}

packagefunc是关键字,两个main是identifier指示器?后面一章会介绍

运行go run:

  • go run xxx.go多文件go run .
  • 不推荐这样去运行大项目

go的操作:

  • go build或者go install去运行大项目

  • go fmt去format一个源代码

  • go get
  • go test测试基准
  • go doc查看go的文档
  • go mod管理依赖
  • go help查看操作

要开始和Go Code打交道了

Introduction to Source Code Elements

啥叫源代码元素啊??

Programming can be viewed as manipulating operations in all kinds of ways to reach certain goals.这句话挺好

The type system of a programming language is the spirit of the language.

开篇讲的东西还是很值得一读的,大致是以编程语言为主线,从最初的低级指令编程是error-prone的到高级语言对低级指令的封装和对数据的抽象能较好的解决错误问题的效率问题,引出数据类型系统的重要,

Named functions, named values (including variables and named constants), defined types and type alias are called resources in Go 101.

变量和包的命名要遵循identifier的标准,这后面会说。

讲到了许多高级语言用packet封装代码,导入一个包要import,同时一个包要export resource。

go里的注释: // or /**/

看代码了

/*
 * @Author: CoyoteWaltz
 * @Date: 2019-11-28 18:39:21
 * @LastEditors: CoyoteWaltz
 * @LastEditTime: 2019-11-28 19:00:43
 * @Description: from go 101 sample
 */
package main // specify the source file's package
// different in other lang

import "math/rand" // import a standard package

const MaxRnd = 186 // a named constant declaration

// A function declaration
/*
 StatRandomNumbers produces a certain number of
 non-negative random integers which are less than
 MaxRnd, then counts and returns the numbers of
 small and large ones among the produced randoms.
 n specifies how many randoms to be produced.
*/
func StatRandomNumbers(n int) (int, int) {
    // Declare two variables (both as 0).
    var a, b int
    // A for-loop control flow.
    for i := 0; i < n; i++ {
        // An if-else control flow.
        if rand.Intn(MaxRnd) < MaxRnd/2 {
            a = a + 1
        } else {
            b++ // same as: b = b + 1
        }
    }
    return a, b // this function return two results
}

// "main" function is the entry function of a program.
func main() {
    var num = 100 // automatically deduce the type
    // Call the declared StatRandomNumbers function.
    x, y := StatRandomNumbers(num)
    // Call two built-in functions (print and println).
    print("Result: ", x, " + ", y, " = ", num, "? ")
    println(x+y == num)
}

// (Note, the built-in print and println functions are not recommended to be used
// in formal Go programming.
// The corresponding functions in the fmt standard packages should be used
// instead in formal Go projects. In Go 101, the two functions are only
// used in the several starting articles.)
// import "fmt"
// fmt.Printf("hello, wodddd\n")

We should try to make code self-explanatory and only use necessary comments in formal projects.这也很重要!

牛逼了,说道花括号可不能像C++一样乱放,很多时候左花括号不能放在行的开始!不过我也习惯是把左括号放在函数声明的后面。书上说这么做可以快速编译,方便gophers阅读代码。快速编译是咋回事?

小结一下: 题目说道的code elements其实就是编程遇到的种种东西,变量、函数。。。。


Keywords and Identifiers

25个关键字

break     default      func    interface  select
case      defer        go      map        struct
chan      else         goto    package    switch
const     fallthrough  if      range      type
continue  for          import  return     var

记一下没见过的:

  • chan interface map struct是在组合类型标记里面用的
  • fallthrough
  • defer go也是控制流里面的关键字,特殊方法,之后说
  • 所有的之后都会详细说明

Identifiers

用来identify变量、package、函数、等等code element的

单下划线_是特殊的identifier,叫blank identifier

注意点:

  • 大写字符开头的是可以被Import,可以看成是这个package可以给别的包public的
  • 其他的都是non-exported的,private
  • 惊了,只要是Unicode Standard 8.0的都可以,中文也行,日文韩文也行.........

Basic Types and Basic Value Literals

来看看基本变量和基本啥literals??

Types can be viewed as value templates, and values can be viewed as type instances.

内置变量:

  • bool
  • 11种int: int8, uint8, int16, uint16, int32, uint32, int64, uint64, int, uint, uintptr。疯了
  • 2种float: float32, float64
  • 2种复数: complex64, complex128 还不太了解
  • string

说这些类型都属于一个go中不同的变量??

两个内置的aliases:

  • byte alias uint8
  • rune alias int32

The size of uintptr, int and uint values n memory are implementation-specific.

complex64的实部和虚部都是float32,同理complex128

string在内存中其实是储存了一堆的byte,也就是uint8,ascii字符8bit

利用关键字type来声明一个identifier是一个内置类型

type status bool
type MyString string

type boolean = bool

感觉就像是C++的typedef或者是using xx = xx

关于0值:

  • bool的false
  • 数值类型的0
  • empty string

Basic Value Literals

A literal of a value is a text representation of the value in code. A value may have many literals.

所谓litreals, the way of a value is represented:

  • int可以用10,8,16进制表示
  • float: 1.12 01.21 1.23e2
  • 16进制float: yPn == y * 2^n,同样16进制的时候不能有e出现
  • 虚数: 1.23i反正就是在后面跟一个小写的i
  • _使数值变量更容易读: 6_9 == 69

Rune value literals

这个rune类型是什么东西呢?

可以自定义这个类型,当然默认的类型就是int32,也算是一个特殊的整型类型吧,可见他是32位长的

可以用rune来表达很多的整型

Generally, we can view a code point as a Unicode character, but we should know that some Unicode characters are composed of more than one code points each. 这个code point不太理解,看成是字节吗?

一般来说大家喜欢用单引号包住字符来表示一个rune类型:

'a'
'n'
'哈' // 中文就是两个字节了 16 bit
// 下面也是 'a' ascii = 97
// 141 is the octal representation of decimal number 97.
'\141'
// 61 is the hex representation of decimal number 97.
'\x61'
'\u0061'
'\U00000061'
// 用 \ 之后必须跟着3个8进制的数来表示一个二进制的值
// \x 之后必须 2位16进制的数
// \u 之后必须4位16进制 to represent a rune value,
// \U 之后必须8位16进制 to represent a rune value,    
// 4位二进制表示一个十六进制数 也就是换一种方式来表示 rune这个类型的值

特殊字符 after \别忘了

\a   (Unicode value 0x07) alert or bell
\b   (Unicode value 0x08) backspace
\f   (Unicode value 0x0C) form feed
\n   (Unicode value 0x0A) line feed or newline
\r   (Unicode value 0x0D) carriage return
\t   (Unicode value 0x09) horizontal tab
\v   (Unicode value 0x0b) vertical tab
\\   (Unicode value 0x5c) backslash
\'   (Unicode value 0x27) single quote

String value literals

字符串在go里面是utf8编码的,go的源文件当然也是utf8编码兼容的啦,不用和python2一样指定编码

两种字符串的literals:

  • 双引号括起来的

  • 用两个`括起来的 raw string 不会转义 raw的输出

    `hello
    ahah "heihei"`

转义字符用的是escape这个单词


Constants and Variables

这篇讲的是constant和variable,常量和变量,关于untyped values和typed values。

Untyped valuse and typed values

有些变量在go中的type是没有被确定的叫做untyped,大多数这些untyped都有一个默认type,但是nil这个是没有默认type的untyped之后会遇到。

所有的literal constant都是untyped。一个constan的默认类型是他的literal form,比如字符串constant"sssttrrr"的类型就是string。

对untyped常量的explicit转换

还挺严格。。。其实也就是说在go里面,常量一般都是没有type的,只有一个default type,也只能用它来转换!

Introduction of type deductions(inference) in go

和python很像,go也是可以自动判别数据类型的。所以很多时候不需要显示的指明数据类型。go编译器会自动根据上下文来判别类型。很多place需要一个有类型的值或者无类型的值,go编译器会根据untyped的value的类型推断这个place的类型,这些place有: 操作符之后的参数,函数的形参,...总之只要是有赋值操作的时候就会自动inference,其他的情况下不需要知道数据的类型,直接将untyped的default type作为类型使用。上面说的两种是隐式转换。

Constant Declarations

  • 一行一个声明

  • 一组一起声明

  • 一行多个声明(和py一样灵活)

The = symbol means "bind" instead of "assign". We should interpret each constant specification as a declared identifier is bound to a corresponding basic value literal.

Please note that, constants can be declared both at package level (out of any function body) and in function bodies. package level go里面是写package作为一个文件比如package main

所以package level的变量就是作用域在这个package的!感觉go的思路很清晰

全局constant这里也可以叫成package-level constants,全局的constant变量的声明顺序不重要。

typed named constants

const X float32 = 3.14
const (
    A, B int64   = -3, 5    // golint告诉你B需要单独声明...
    Y    float32 = 2.718    // golint告诉你这样不规范 一定要在上面注释告诉别人Y是什么
)

其他的见代码部分

在group模式的时候声明constant的时候可以不用完整的声明,e.g.

const (
    X float32 = 3.14
    Y           // here must be one identifier
    Z           // here must be one identifier

    A, B = "Go", "language"
    C, _
    // In the above line, the blank identifier
    // is required to be present.
)
// <====>
const (
    X float32 = 3.14
    Y float32 = 3.14
    Z float32 = 3.14
    A, B = "Go", "language"
    C, _ = "Go", "language"
)

iota?

iota(一个generator)是一个constant声明的另一个特性,它是被predeclared的(const iota = 0),const iota只能被用在其他的constant declarations,每次在group constant声明的时候被使用一次就自增1,看一下go101的例子

package main

func main() {
    const (
        k = 3 // now, iota == 0

        m float32 = iota + .5 // m float32 = 1 + .5
        n                     // n float32 = 2 + .5

        p = 9             // now, iota == 3
        q = iota * 2      // q = 4 * 2
        _                 // _ = 5 * 2
        r                 // r = 6 * 2
        s, t = iota, iota // s, t = 7, 7
        u, v              // u, v = 8, 8
        _, w              // _, w = 9, 9
    )
    const x = iota // x = 0
    const (
        y = iota // y = 0
        z        // z = 1
    )
    println(m)             // +1.500000e+000
    println(n)             // +2.500000e+000
    println(q, r)          // 8 12
    println(s, t, u, v, w) // 7 7 8 8 9
    println(x, y, z)       // 0 0 1
}

有iota之后可以很巧妙便捷的声明,So iota is only useful in group-style constant declarations.

实战中我们可以这样巧用iota: 牛逼嗷

const (
    Failed = iota - 1 // == -1
    Unknown           // == 0
    Succeeded         // == 1
)
const (
    Readable = 1 << iota // == 1
    Writable             // == 2
    Executable           // == 4
)

变量、变量的声明和变量的赋值

声明变量的时候要给go编译器提供多一点的信息为了deduce类型

package-level variables V.S. local variables

妈的还有两种基本的声明格式,标准和简短,后者只能用来声明loacal变量

Standard variable declaration forms

  • 用var关键字开头,变量名,类型,值都是specified的

  • var lang, website string = "Go", "xxx"

  • 多变量可以一起,但是要注意他们必须是同一个类型

  • Full standard variable declaration forms are seldom used in practice, since they are verbose.

  • 呵呵

  • 有两个标准声明的变形: 也就是利用自动推导类型

    • 不显示给类型,一起声明的变量可以不同类型var lang, dynamic = "Go", false go是静态..编译

    • 不给值的声明,但是要给定类型,编译器自动初始化值

      // Both are initialized as blank strings.
      var lang, website string
      // Both are initialized as false.
      var interpreted, dynamic bool
      // n is initialized as 0.
      var n int
  • group也可以

  • 变量声明了你不用,go会报错,呵呵呵

  • Generally, declaring related variables together will make code more readable.

Pure value assignments

等号在改变变量的时候赋值叫做纯值赋值......

一个下划线_`叫做blank identifier,可以用作被赋值的对象,也就是忽略输出,但不能放在等号右边

强类型语言,注意类型match

注意go不支持链式赋值a = b =13不行!

Short variable declaration forms

  • 大开眼界啊妈的
  • lang, year := "sss", 2200 注意这是声明 !
  • redeclare: year, sss := 111, "sss" 注意这里的year仅仅是改变了值,不能改变类型
  • 声明行必须只少声明一个新的变量,不然直接给他赋值就行了
  • 与标准声明的区别:
    • var和类型必须omitted
    • 必须:=
    • 如果是新老变量可以一起在声明行中,也就是必须至少有一个新声明,才能用:=

谈谈Assignment

x is assignable to y which means if statement x = y is legal (compiles okay)此时的y类型为Ty,那么可以说x is assignable to type Ty,这里y与x类型相同或者可以是隐式转换,同时y可以是_

and 局部变量声明之后需要被至少用一次....但是全局的变量没有这样的限制,所谓的be effectively used是指最起码被放在等号左边....被当做赋值来用,实在不行就_ = unused生产模式别搞..debug可以搞搞

Dependency relations of package-Level variables affect their initialization order

前面说到了全局变量的声明是可以不管顺序的,看看他们之间的相互依赖会产生的影响吧

var x, y = a+1, 5         // 8 5
var a, b, c = b+1, c+1, y // 7 6 5

先看能直接赋值的y=5-->c=y=5-->b=6-->a=b+1=7-->x=a+1

声明的时候循环赋值(loop/circularly)就不行了var x, y = y, x编译不过

Value Addressability

所有变量都有地址可寻,然而constant都没有的,之后的指针篇会学更多

Explicit Conversions on Non-Constant Numeric ValuesExplicit Conversions on Non-Constant Numeric Values

显示类型转换 on 非const变量,有以下几个点

  • 除了int到str还有浮点数到int,任意复数之间
  • overflow是可以的,也就是会被近似(rounding)

Scopes of Variables and Named Constants

A variable or a named constant declared in an inner code block will shadow the variables and constants declared with the same name in outer code blocks.这句话很有意思,在内部的block,也就是{}里的变量会shadow(遮蔽)外部同样名称的变量,后面文章会详细讲。

More About Constant Declarations

untyped的constant可以在声明的时候overflow,但在用的时候必须处理成不会overflow。

有名字的constant在被编译的时候会被替换成他的值。可以看成是C语言中的加强版宏定义。


Common Operators

数学,位操作,比较,布尔,字符串拼接算子。都是二元或者一元(unary)算子,且都返回一个值

Arithmetic Operators

  • 算术运算符就和cpp一样,除法要注意类型,int类型就是整除
  • 位运算中有一个bitwise clear: x &^ y: 按y的位将x置零: 返回y的1所在位将x对应位置为0,也就是先取反y,再和x与。这是go独有的,m &^ n is equivalent to m & (^n).
  • +也可以用于string拼接
  • *&也用于指针,like c and cpp
  • 不像java,无符号移位>>>这个没有
  • power operator没有基础款,只能用在mathpackage中的Pow函数

rune和int在操作符两边之后会返回rune的类型。

位运算符(binary)的返回值类型的规则:(先记下来吧,到时候细看)

  • 通常来说返回的都是int

  • 如果左参数是有特定类型的,返回类型也是他

  • 如果左参数untyped,右边是一个constant,那么左参数会被当做是int类型,如果他的默认类型不是int,那么必须要可以是转换成int的类型。返回结果也是一个untyped的,默认类型是和左参数一样

  • 如果左参数untyped,右边是non-constant,那么左参数会先被转换成一样类型的,返回值是一个有类型的值。

    var m = uint(32)
    var z = int64(1) << m
    /*
    if the operand 1 is deduced as int instead of int64, the bitwise operation at line 13 (or line 12) will return different results between 32-bit architectures (0) and 64-bit architectures (0x100000000), which may produce some bugs hard to detect.
    */

感觉go挺强调变量的类型的,因为不像cpp/c对类型如此严格的声明,go更灵活,但也要求我们要对变量的类型清楚的不得了。

关于overflow

溢出不允许发生在typed constant,但是可以是non-constant和untyped constant,为什么呢。

首先变量的溢出直接按照内存给的位不够了还好理解,untyped-constant允许溢出个人认为是由于上面提到了const相当于是宏定义,那么即使在声明untyped常量的表达式是会溢出的那只要在编译替换的时候整个表达式不溢出就ok了,所以允许,但是typed常量明显不允许了。

var a, b = 4.0, 0.0
println(a / b) // +Inf
// println(0 / 0)

上面是可以通过编译的,说是分母为int0但不是constant的时候会引起一个run-time panic,相当于是其他语言的error,之后会学到panics。

go支持op=,++, --,但是自增自减只能是后缀,对象必须是数值类型(小数也可以),且不返回任何值!

String concatenation

+即可,同样可以+=

Boolean operators

和cpp一样,两边有一个是typed那么结果也是typed的,如果两边都是untyped,那么结果untyped

Comparison operators

一样,返回值一般都是untyped,如果两边都是constant,那么返回也是const bool。注意not all real numbers can be accurately represented in memory, so comparing two floating-point (or complex) values may be not reliable. We should check whether or not the absolution of the difference of two floating-point values is smaller than a small threshold to judge whether or not the two floating-point values are equal.

Operator precedence(优先级吧)

*   /   %   <<  >>  &   &^ // 按行以此递减 行中一样 按从左到右来
+   -   |   ^               // 与其他不一样 << >> 比 + -优先级高
==  !=  <   <=  >   >=
&&
||
const x = 3/2*0.1
const y = 0.1*3/2

func main() {
    println(x) // +1.000000e-001
    println(y) // +1.500000e-001
}

Function Declarations and Function Calls

写函数啦!函数操作符(function opeation)通常叫做函数调用(function call)

函数声明

func SquaresOfSumAndDiff(a int64, b int64) (s int64, d int64) {
    x, y := a + b, a - b
    s = x * x
    d = y * y
    return // <=> return s, d
}

函数签名: 关键字func 函数名 输入参数() return的参数() 函数体{}

  • go函数可以返回多个结果

  • 输入和输出参数的形式可以看成是没有var的标准类型声明。

  • 返回的变量声明(list)的名字必须一起出现或者没有,两者都可以。如果result是有name的,被叫做named result,否则叫anonymous result

    func SquaresOfSumAndDiff(a int64, b int64) (int64, int64) {
      return (a+b) * (a+b), (a-b) * (a-b)
    }
  • 如果参数没有在函数体中被使用,那么他们的名字也可以没有,anonymous parameters实际中很少用。

  • go不支持参数的默认值

  • 如果连续的(successive)参数或者result的类型是一样的,可以省略func SquaresOfSumAndDiff(a, b int64) (s, d int64)andfunc SquaresOfSumAndDiff(a, b int64, flag1, flag2 bool) (s, d int64)

  • 返回值是一个的时候可以省略(),但是类型一定要给,如果没有返回值....,但是参数的()必须要有

    // Foo .
    func Foo(x int, b bool) int {
      println(x, b)
      return 1
    }
  • 函数声明必须在package level中,在函数体内的只可以是匿名函数,但这不叫函数声明

Function calls

就一般的用就行。函数的声明可以是写在调用之后的!感觉是编译的时候把所有的func排序?main放最后??我猜的。

Function calls can be deferred or invoked in new goroutines (green threads) in Go. Please read a later article for details.

Exiting phase

就是函数return的时候并没有结束,而是进入了一个退出阶段,这个之后还会讲到。

Anonymous Functions

定义一个匿名函数几乎和一个函数声明是一样的,只不过他是没有name的。

x, y := func() (int, int) {
    println("This function has no parameters.")
    return 3, 4
}() // Call it. No arguments are needed.
// 这里直接call匿名函数对x和y进行了初始化

func(a, b int) {
    // The following line prints: a*a + b*b = 25
    println("a*a + b*b =", a*a + b*b)
}(x, y) // pass argument x and y to parameter a and b.

func(x int) {
    // The parameter x shadows the outer x.
    // The following line prints: x*x + y*y = 32
    println("x*x + y*y =", x*x + y*y)
}(y) // pass argument y to parameter x.

func() {
    // The following line prints: x*x + y*y = 25
    println("x*x + y*y =", x*x + y*y)
}() // no arguments are needed. 这个匿名函数是可以直接用到x和y的

In fact, all custom functions in Go can be viewed as closures. This is why Go functions are as flexible as many dynamic languages.

内置函数

后文还会提到

println

print

real

imag

complex

如果参数有一个是constant,返回值也是constant,同理untyped

// c is a untyped complex constant.
const c = complex(1.6, 3.3)

// The results of real(c) and imag(c) are both
// untyped floating-point values. They are both
// deduced as values of type float32 below.
var a, b float32 = real(c), imag(c)

// d is deduced as a typed value of type complex64.
// The results of real(d) and imag(d) are both
// typed values of type float32.
var d = complex(a, b)

// e is deduced as a typed value of type complex128.
// The results of real(e) and imag(e) are both
// typed values of type float64.
var e = c

Code Packages and Package Imports

代码package和package的导入

通常我们可以看到一个go文件里面的代码是这样的(比如xx.go)

package main

import "fmt"

func main() {
    fmt.Println("Go has", 25, "keywords.")
}
  • main入口函数必须要放在名字叫做main的package中。
  • import关键字之后的fmt是一个standard package,这个identifier fmt在这个代码文件xx.go的作用域中也是这个package的名字。
  • fmt.Println: The form aImportName.AnExportedIdentifier is called a qualified identifier.(需要科网访问golang.org)
    • 这个函数has no requirements for its arguments(?),反正会自动判断参数的类型,打印在一行,用空格分开。

Please note, only exported resources in a package can be used in the source file which imports the package.翻译一下: 这个文件模块里面的资源能被import使用的条件是,这些资源是被exported了的。

什么样的变量是可以被exported的呢之前讲identifier的时候提到过,也就是首字母大写的变量名是exported identifier,也就是说这个变量是public属性的。

所以看fmt.Println这个函数只要是能被import过来用的,必定是首字母大写的函数

All standard packages are listed here.(科网访问),以后用到的时候在学

A package import is also called an import declaration formally in Go. An import declaration is only visible to the source file which contains the import declaration. It is not visible to other source files in the same package.在一个package中import的操作只会在这个package中起作用(visible),这应该都是默认知道的。

例子:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    rand.Seed(111)
    fmt.Println(time.Now())
    // fmt.Println(time.Date(2010, 11, 1, 23, 12, 12, 12, "8"))
    fmt.Println(rand.Uint32(), rand.NormFloat64(), rand.NormFloat64(), rand.NormFloat64())
    fmt.Printf("printf vvv %v\n", func() int { return 123 }())
    // 这里用了一个匿名函数 看来是可以的 go会自动判断类型给到%v
}

来看看这个fmt.Printf函数,就是c语言的printf,看看这里的占位符%v,v表示一个 format verb,是不是第二个参数只要是一个函数就行呢?(后面就详细谈一谈)

随机数种子,time包里面的Now原来可以这样用,转换为unix时间

rand.Seed(time.Now().UnixNano())
fmt.Println(time.Now().UnixNano())
// 哈哈原来这样用啊 time包的Time类型可以有这样一个成员函数转换为unix时间
// 这个unix时间是以微妙计数的整数int64

More about fmt.Printf format verbs

这个函数的第一个参数中只要有%v这样的format verb,就会被第二个参数转换为字符串,来看看有多少个占位符:(这本书里只讲一点点)

  • %v: will be replaced with the general string representation of the corresponding argument.

    • 这个是一个general的占位符,就是说不管给什么类型都行,如果是struct,那么会打印{attr1 attr2 ...},如果加了%+v会打印属性名称
    // 只有被exported的变量才需要comment
    type tt struct {
      name string
      age  int8
    }
    func main() {
      fmt.Printf("%v\n", tt{"ss", 12})
    }
    // {ss 12}
    // 加了%+v就是 {name:ss age:12}
  • %T: 替换为这个变量的类型!

  • %x: 被替换为hex的string,一般都是integer,integers array or integer slices

  • %s: string或者byte slice(?) 字节片?

  • %%: %

  • %t: 布尔值的true or false

  • %b    base 2
    %c    the character represented by the corresponding Unicode code point
    %d    base 10
    %o    base 8
    %O    base 8 with 0o prefix
    %q    a single-quoted character literal safely escaped with Go syntax.
    %x    base 16, with lower-case letters for a-f
    %X    base 16, with upper-case letters for A-F
    %U    Unicode format: U+1234; same as "U+%04X"

Package Folder, Package Import Path and Package Dependencies(需要深入)

For example, package .../a/b/c/internal/d/e/f and .../a/b/c/internal can only be imported by the packages whose import paths have a .../a/b/c prefix.

这段前面说的就是一个文件夹下有好多package,这些package属于这个folder,balabalabala。。。

In Go SDK 1.11, a modules feature was introduced. A module can be viewed as a collection of packages which have a common root (a package tree). Each module is associated with an root import path and a semantic version. The major version should be contained in the root import path, execpt the v0 or v1 major versions. Modules with different root import paths are viewed as different modules.

If a package is contained within a GOPATH/src directory, and the modules feature is off, then its import path is the relative path to either the GOPATH/src directory or the closest vendor folder which containing the package.就是说当module特性关闭的时候,也就是一个package在GOPATH/src中(export | grep GO查看GOPATH)的时候,那么这个包被引入的路径就是src路径或者是最近的vendor中,这个vendor其实是govendor包管理工具创建的文件夹,存放模块的,之后应该会学到的。

When the modules feature is on, the root import path of a module is often (but not required to be) specified in a go.mod file which is directly contained in the root package folder of the module. We often use the root import path to identify the module. The root import path is the common prefix of all packages in the module.

Only the vendor folder directly under the root path of a module is viewed as a special folder.才会触发module特性,被引用的package会在这个root下的vendor找

之后会谈main入口函数,它被称为program packages or command packages,其他的package就叫做library packages每一个go程序只能有一个program package

So please try to make the two names identical for each library package.

这一部分关于go.mod的使用方法之后一定会去看的!很重要

(补充)Semantic Versioning 版本命名方法

The init Functions

初始化方法??

每一个package可以有多个init方法,他必须是没有输入和输出的。

注意在package-level的地方init这个identifier只能被用在函数声明,不能用它当做变量等的声明。

在run time的时候每一个初始化函数都会被按序激活一次,仅一次,且是在main入口函数之前。

虽然还不知道init函数的妙用。。感觉目前这本go101只是粗略带过。

Resource Initialization Order

资源初始化顺序,哦?也许可以印证我上面的猜测。

首先,在运行的时候,一个package会在import他所有的dependency package之后加载。

所有的init方法都会被按顺序调用,全部的init会在main之前调用,并且会在所有被import的包的init调用之后调用。

So it is not a good idea to have dependency relations between two init functions in two different source files.

总的来说,都是先初始化import进来的package,再按照顺序(如果特殊情况赋值的话就不按顺序)初始化

Full Package Import Forms

完整的package引入格式是

import importname "path/to/package"

importname相当于是给package其别名

import fmt "fmt"        // <=> import "fmt"
import rand "math/rand" // <=> import "math/rand"   // 其实就是最后的package名字
import time "time"      // <=> import "time"

这个别名其实也不怎么常用,有的情况是有两个包有相同的名字,但在不同的路径下需要加以区分

如果这个importname是一个点.,那么,相当于没有importname,from xxx import *这种感觉,直接使用package中的exported对象。但是写过python也就知道这样不太推荐。

同时也可以是_,称为anonymous imports或者blank imports,但是这样之后,在文件中不能使用anonymous import的exported资源,这样引入_的目的只是为了激活这个package中的init函数

最后要记得,非anonymous import必须被使用至少一次,不然ctrl+s的时候golint直接帮你把这行删了。。。


Expressions, Statements and Simple Statements

Simply speaking, an expression represents a value and a statement represents an operation.

简单的Expression的cases

这本书里谈到的表达式都是single-value expression,就是这一个表达式返回一个值。

抛出了很多topic,methods,channel,自定义的function,之后都会学到。所以go里面,函数和方法是不同的两个东西哈。

简单的Statement Cases

  • 变量声明,短的,不完整的那种
  • 纯值赋值语句,包括x op= y
  • 函数/方法的调用,channel接收的参数
  • channel send operations
  • nothing (a.k.a., blank statements). We will learn some uses of blank statements in the next article.
  • a++, a--

一些不是simple statement的情况

  • 完整的变量声明
  • named常量声明
  • 自定义类型声明
  • package导入声明
  • 语句块
  • 函数声明
  • 控制流和代码执行跳转??jumps??
  • return的那行
  • deferred function calls and goroutine creations. The two will be introduced in the article after next

虽然看到这里觉得讲expression和statement没什么意义。。也许之后会有意义吧。。。


Basic Contril Flows

基础控制流语法,应该很简单,大致上有:

  • if-else
  • for
  • switch-case
  • for-range: loop block for container types
  • type-switch: multi-way conditional execution block for interface types.这又抛出个interface类型。。
  • select-case: for channel types

go支持break,continue,goto还有一个特殊的跳出语句fallthrough,除了if-else,都是breakable control flow blocks

注意每个控制流block都是一个statement,里面可以有其他的sub-statement

if-else

if InitSimpleStatement; Condition {
    // do something
} else if ... {
    // do something
} else {
    // ...
}

InitSimpleStatement 这个初始化的简单声明是可选的。实战中通常是一个纯值赋值或者变量声明。如果有初始化声明,那就不能在Condition和他外面加(),如果加了就把他们看成一个表达式了呗。没有初始声明的话,分号是optional的。

for (go 没有 while)

for InitSimpleStatement; Condition; PostSimpleStatement {
    // do something
}

三个部分都是optional,这三部分都不能被(),同样分号可以省略,那就相同与while了。没有Condition的时候会被当做是true

for i := 0; i < 3; i++ {
    fmt.Print(i)
    // The left i is a new declared variable,
    // and the right i is the loop variable.
    i := i
    // The new declared variable is modified, but
    // the old one (the loop variable) is not yet.
    i = 10
    _ = i
}

看一下这段,很好的说明了{}的作用,在这里面声明的新变量i是不会被外面的for条件给用到的,出了循环就没了。

switch-case

switch InitSimpleStatement; CompareOperand0 {
case CompareOperandList1:
    // do something
case CompareOperandList2:
    // do something
...
case CompareOperandListN:
    // do something
default:
    // do something
}

注意这里的case对象可以都是list,就是用逗号分开的变量,和CompareOperand0是可比的

Note, if any two case expressions in a switch-case control flow can be detected to be equal at compile time, then a compiler may reject the latter one.所谓reject就是直接报错,也就是两个case中不能出现相同的东西,不然就匹配到两个结果了,事实上目前go1.13编译器允许这样。。

注意,和c++不同,go的switch-case会自动在case后面break掉,如果还想继续往下溜,那么就要用到fallthrough这个关键字了!

rand.Seed(time.Now().UnixNano())
switch n := rand.Intn(100) % 5; n {
case 0, 1, 2, 3, 4:
    fmt.Println("n =", n)
    // The "fallthrough" statement makes the
    // execution slip into the next branch.
    fallthrough
case 5, 6, 7, 8:
    // A new declared variable also called "n",
    // it is only visible in the currrent
    // branch code block.
    n := 99
    fmt.Println("n =", n) // 99
    fallthrough
default:
    // This "n" is the switch expression "n".
    fmt.Println("n =", n)
}

注意那行变量声明的n := 99,这个n是在case的implicit block里面,local的

fallthrough必须是branch的最后一个statement,而且不能出现在最后一个branch中

如果CompareOperand0也被省略的话还是被当做是ture

和其他的不同,default语句可以在随意的顺序

goto

goto必须跟着一个LabelName,一个label是LabelName:,举个例子

func main() {
    i := 0

Next: // here, a label is declared.
    fmt.Println(i)
    i++
    if i < 5 {
        goto Next // execution jumps
    }
}

Note that, if a label is declared within the scope of a variable, then the uses of the label can't appear before the declaration of the variable.

break and continue Statements With Labels

package main

import "fmt"

func FindSmallestPrimeLargerThan(n int) int {
Outer:
    for n++; ; n++{
        for i := 2; ; i++ {
            switch {
            case i * i > n:
                break Outer
            case n % i == 0:
                continue Outer
            }
        }
    }
    return n
}

func main() {
    for i := 90; i < 100; i++ {
        n := FindSmallestPrimeLargerThan(i)
        fmt.Print("The smallest prime number larger than ")
        fmt.Println(i, "is", n)
    }
}

Goroutines, Deferred Function Calls and Panic/Recover

这,来要来点硬核的东西了!开始吧。

这篇长文将介绍goroutine协程,deferred function调用。是go的两个很unique的特性。

同时也会解释panic和recover机制?

Goroutines

回忆一下操作系统,当代计算机的CPU都是多核心,支持hyper-threading,嗯?超线程。所谓超线程是intel的cpu新特性,不过好像第九代就抛弃了,是通过一个cpu内核的2个architecture state软件方法模拟实现出两个逻辑内核,使得两个超线程可以同时得到cpu的资源。

Goroutines就是go语言实现concurrent(并发)计算的东西。也成为green threads,绿色线程。不过呢,这个绿色线程由go语言运行的时候来调度和维护的,而不是交给os去控制。想到了上次看的b站视频讲的家庭模式和旅馆模式的区别,简单的比喻来说就是家里人睡的房间要是换个家里人睡,不用彻底大扫除,而旅馆每换一个客人都要彻底的大扫除,而这大扫除的操作就是os新建新线程和切换上下文的开销。

如果每个线程都是os拉起来的,每个线程的大小都要2mb还是4mb来着?特别吃内存,还有nodejs的epoll方法,event stack,这里先不多说了,总之node是很牛逼的单线程,非阻塞IO和事件驱动的调度能手。

go不支持用户用代码新建系统的线程。所以只有用goroutines才能在go里面并发编程。

go关键字后跟一个function call就是新建一个新线程的操作了。新的goroutine会在函数调用之后退出。

go程序的开始就是一个goroutine,在程序里面用go创建的都算是他的sub-goroutine,看代码熟悉一下

package main

/*
 * @Author: CoyoteWaltz
 * @Date: 2020-01-14 21:07:56
 * @LastEditors  : CoyoteWaltz
 * @LastEditTime : 2020-01-15 11:21:46
 * @Description: goroutine from go101
 */

import (
    "log"
    "math/rand"
    "time"
)

// 不用大写 不用被exported 不用写顶行注释 舒服
func greeting(greeting string, times int) {
    for i := 0; i < times; i++ {
        log.Println(greeting) // 这里不用fmt的原因 因为 log中的是synchronized
        // fmt.Println(greeting, i) // fmt的会异步 也许会打印在同一行 这个程序几乎不会出现。。
        delay := time.Second * time.Duration(rand.Intn(3)) / 2
        // time.Duration 其实就是在time模块里面自定义的一个类型 即 int64
        time.Sleep(delay)
    }
}

func main() {
    // main goroutine
    rand.Seed(time.Now().UnixNano())
    log.SetFlags(0)
    go greeting("yes", 100)
    go greeting("no", 50)
    time.Sleep(30 * time.Second) // 主线程存活的时间 如果main退出了 其子goroutine都死亡 anyway
}

Concurrency Synchronization

并发同步

看一些并行(发)计算的data races情况:

  • 同时,读和写同一个memory segment。读的数据的integrity不能保证。
  • 两个计算同时写同一个memory segment

所以并发编程要控制这种数据冲突,to implement this duty,叫data synchornization or concurrency synchornization。以及还有需要注意的:

  • 决定多少个并发计算
  • 决定一个计算什么时候开始、阻塞、开塞、结束
  • 决定如何分布荷载

and 如何让main goroutine知道子线程都结束了呢。这些在concurrency synchronization techniques中会介绍。(how to control concurrent computations),这里面还有channel技术,后面会讲。

这里呢根据上面的程序,我们简单的使用sync标准库的WaitGroup来实现主goroutine和两个新的goroutine之间的同步(主进程等待两个结束之后才结束),

package main

import (
    "log"
    "math/rand"
    "sync"
    "time"
)

var wg sync.WaitGroup // 这是一个类型

// 不用大写 不用被exported 不用写顶行注释 舒服
func greeting(greeting string, times int) {
    ...
    wg.Done() // <=> wg.Add(-1)
}

func main() {
    // main goroutine
    rand.Seed(time.Now().UnixNano())
    log.SetFlags(0)
    wg.Add(2) // register two tasks
    go greeting("yes", 10)
    go greeting("no", 10)
    // time.Sleep(30 * time.Second) // 主线程存活的时间 如果main退出了 其子goroutine都死亡 anyway
    wg.Wait() // block until all tasks are finished
    log.Println("main done")
}

Goroutine States

goroutine存在两个状态,running和blocking。在上面的例子,wg.Wait()调用的时候main goroutine就进入了阻塞态,直到两个新线程都结束之后才重新进入running态。

goroutine就和os的线程一样,走走停停,在两个状态中来回切换。

注意,调用time.Sleep函数或者在等待系统函数调用的回应或者网络连接,这些都算是在运行态。(在go101里是这样的)

只能在running state中exit,每次被create起来先进入的是running。

一个阻塞了的goroutine只能在另一个goroutine中被重新拉回running,要是所有的goroutine都在阻塞,那么他们就都凉凉。看成是一个全体死锁。如果出现这样的情况,标准Go runtime会尝试crash the program。看个例子:

package main

import (
    "sync"
    "time"
)

var wg sync.WaitGroup

func main() {
    wg.Add(1)
    go func() {
        time.Sleep(time.Second * 2)
        wg.Wait()
    }()
    wg.Wait()
}

fatal error: all goroutines are asleep - deadlock!报错咯。

Goroutine Schedule

被执行的最多的goroutine数量不会超过cpu提供的所有逻辑上可以使用的内核个数。runtime.NumCPU函数调用可以获得可用的cpu数量。

go runtime就好比是cpu调度中心,一直在调度goroutine,让他们每个都有机会被cpu执行。

goroutine在running的时候是排队等待被cpu执行,此时应该cpu数量小于goroutine的数量。

标准Go runtime采用了MPG模型来做goroutine的任务调度。很重要,复习一下操作系统,单cpu,单cpu多核,多cpu单核,进程是啥,线程是啥,协程coroutine是什么。

MPG Model

接着说MPG,M指的是Machine,是一个os的线程,P指的是logical processor也就是处理器核,逻辑或者虚拟的(因特尔的超线程),G就是goroutine了。调度工作也就是将G添加到一个M的工作队列。一个os线程在给时间片处理(os分配的)的时候最多被attached一个goroutine,同时一个goroutine在有时间片(go调度器分配的)的时候最多也只能attached到一个os线程上。所以呢只有当goroutine在os线程上才可以被执行,到了时间片结束或者有其他的情况(io, channel, wait, runtime.Gosched()),goroutine会自己让出线程的控制权,让线程去执行其他的goroutine。想象一下这张图,os控制的线程P下有好多个goroutine,当一个P关联多个G的时候,此时就是并发,同一个时间段要处理多个G,通过MPG模型来实现百万协程的并发。

我的理解就是,go的调度器相当于是把一个线程当做了一个cpu,充当os的角色让协程goroutine成为了线程,进行调度,实现了多级调度?

调用runtime.GOMAXPROCS来获得和设置logical processors的数量,自从go1.5以后,这个默认值就是系统可用逻辑处理器的数量了。

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Println(runtime.NumCPU())       // 8 可用的cpu核心数
    fmt.Println(runtime.GOMAXPROCS(5))  // 设置新的P的数量 但是返回的还是之前的 8
    fmt.Println(runtime.GOMAXPROCS(-1)) // 传入的参数小于1 就不设置 返回previous 5
    // 后者才是关键的P
}

At any time, the number of goroutines in the executing sub-state is no more than the smaller one of runtime.NumCPU and runtime.GOMAXPROCS.总之被执行的goroutine的数量的上界是这两者小的。

Deferred Function Calls

defer这个单词,好吧忘了,是延迟拖延的意思。

这个延迟函数调用的用法是在函数调用之前加一个defer关键字。

Like goroutine function calls, all the result values of the function call (if the called function returns values) must be discarded in the function call statement.这句话我没搞明白啊啥叫discard呢?实验了一下就是go func calls之后的返回值就无效了,也不能加vv := go ..这种。

被冠以defer之名的函数调用不会立即执行,而是被压入一个defer-call栈中,由调用他的goroutine所维护,当这个函数(goroutine在执行)return并进入exiting phase的时候,所有被压入栈的deferred function才被执行,出栈执行,也就是逆序执行了,所有的这些被执行完才是这个goroutine所在的function真正exit。

package main

import "fmt"

func main() {

    defer fmt.Println(1) //不出意外的话他是最后执行的
    defer fmt.Println(2)
    fmt.Println(3)
} // 3 2 1

实际上每个goroutine都维护两个stack,一个是normal-call stack一个就是defer-call stack

  • For two adjacent function calls in the normal-call stack of a goroutine, the later pushed one is called by the earlier pushed one. The earliest function call in the normal-call stack is the entry call of the goroutine.
  • The function calls in the defer-call stack have no calling relations.

解释一下第一条这个normal-call栈是啥,就是在goroutine执行时候顺序扫描代码,一个func被扫描,就压入nromal-call栈,然后进入func继续扫描到一个其他的函数调用就再压入栈,那么前一个就是后一个的caller,然后递归进入函数扫描,不断入栈,就形成了在stack中相邻的两个函数有这样被调用的关系,那么栈底的那个函数就是entry call了。

而defer-call stack的所有函数的caller都是这个goroutine。

别搞了,用debug.PrintStack()来看看normal-call stack吧

package main

/*
 * @Author: CoyoteWaltz
 * @Date: 2020-01-15 22:30:10
 * @LastEditors  : CoyoteWaltz
 * @LastEditTime : 2020-01-15 22:35:01
 * @Description:
 */

import (
    "fmt"
    "runtime/debug"
)

func test1() {
    test2()
}
func test2() {
    test3()
}
func test3() {
    fmt.Println(333)
    debug.PrintStack()
}

func main() {
    test1()
    test2()
    debug.PrintStack()

}
/*
333
goroutine 1 [running]:
runtime/debug.Stack(0x4, 0x0, 0x0)
        /usr/local/go/src/runtime/debug/stack.go:24 +0x9d
runtime/debug.PrintStack()
        /usr/local/go/src/runtime/debug/stack.go:16 +0x22
main.test3()
        /home/coyotewaltz/Programming/go/w5/call_stack.go:24 +0x7a
main.test2(...)
        /home/coyotewaltz/Programming/go/w5/call_stack.go:20
main.test1(...)
        /home/coyotewaltz/Programming/go/w5/call_stack.go:17
main.main()
        /home/coyotewaltz/Programming/go/w5/call_stack.go:28 +0x22
-------------分割线 上面是test1执行最后栈的情况
333
goroutine 1 [running]:
runtime/debug.Stack(0x4, 0x0, 0x0)
        /usr/local/go/src/runtime/debug/stack.go:24 +0x9d
runtime/debug.PrintStack()
        /usr/local/go/src/runtime/debug/stack.go:16 +0x22
main.test3()
        /home/coyotewaltz/Programming/go/w5/call_stack.go:24 +0x7a
main.test2(...)
        /home/coyotewaltz/Programming/go/w5/call_stack.go:20
main.main()
        /home/coyotewaltz/Programming/go/w5/call_stack.go:30 +0x28
(base) coyotewaltz@coyote-ubuntu:~/Programming/go/w5$ go run call_stack.go 
-------------分割线
333
goroutine 1 [running]:
runtime/debug.Stack(0x4, 0x0, 0x0)
        /usr/local/go/src/runtime/debug/stack.go:24 +0x9d
runtime/debug.PrintStack()
        /usr/local/go/src/runtime/debug/stack.go:16 +0x22
main.test3()
        /home/coyotewaltz/Programming/go/w5/call_stack.go:24 +0x7a
main.test2(...)
        /home/coyotewaltz/Programming/go/w5/call_stack.go:20
main.test1(...)
        /home/coyotewaltz/Programming/go/w5/call_stack.go:17
main.main()
        /home/coyotewaltz/Programming/go/w5/call_stack.go:28 +0x22
333
goroutine 1 [running]:
runtime/debug.Stack(0x4, 0x0, 0x0)
        /usr/local/go/src/runtime/debug/stack.go:24 +0x9d
runtime/debug.PrintStack()
        /usr/local/go/src/runtime/debug/stack.go:16 +0x22
main.test3()
        /home/coyotewaltz/Programming/go/w5/call_stack.go:24 +0x7a
main.test2(...)
        /home/coyotewaltz/Programming/go/w5/call_stack.go:20
main.main()
        /home/coyotewaltz/Programming/go/w5/call_stack.go:30 +0x28
goroutine 1 [running]:
runtime/debug.Stack(0x1, 0x1, 0x4)
        /usr/local/go/src/runtime/debug/stack.go:24 +0x9d
runtime/debug.PrintStack()
        /usr/local/go/src/runtime/debug/stack.go:16 +0x22
main.main()
        /home/coyotewaltz/Programming/go/w5/call_stack.go:31 +0x2d
*/

可以看出normal-call stack实际上记录的是函数调用嵌套的顺序,最先入栈的总是main,然后每执行一个call就记录一下里面的调用情况,通过出栈来return到上一层。

Deferred Function Calls Can Modify the Named Return Results of Nesting Functions

注意是nesting function的named return,翻译一下就是在main外写的func的签名中的有名字的return

package main

import "fmt"

func triple(n int) (r int) {
    defer func() {
        r += n
    }() // 将这个匿名函数的执行defer到最后

    return n + n // 等价于 r = n + n 然后再return
}

func main() {
    r := triple(10)
    fmt.Println(r)
}

返回的r还是会在deferred function被改变

The Necessary and Benefits of the Deferred Function Feature

来看看deferred function的必要性和好处。说实话大部分被defer的function都可以不用这样做,但是!

对于接下来要遇到的panic和recover机制是非常必要的特性。

The Evaluation Moment of the Arguments of Deferred and Goroutine Function Calls

延迟函数或者goroutine function的参数evaluation在其被调用(invoked)的时候。

  • invocation moment调用时间??是被压入defer-call stack的时候,就是压入栈的时候就算是调用这个动作了?
  • For a goroutine function call, the invocation moment is the moment when the corresponding goroutine is created.

The expressions enclosed within the body of an anonymous function call, whether the call is a general call or a deferred/goroutine call, will not be evaluated at the moment when the anonymous function call is invoked.就是匿名函数的参数评估(我觉得这里应该)是until函数被调用。

package main

import "fmt"

func main() {
    func() {
        for i := 0; i < 3; i++ {
            defer fmt.Println("a:", i)
        }
    }()
    fmt.Println()
    func() {
        for i := 0; i < 3; i++ {
            defer func() {
                fmt.Println("b:", i) // 这里和上面的区别是吧print放在了一个匿名函数里面,
                // 内匿名函数在外面的匿名函数中被调用,但是是defer的,压入栈了,出栈的时候i已经都是3了
            }()
        }
    }()
    fmt.Println()
    func() {
        for i := 0; i < 3; i++ {
            defer func(i int) {
                fmt.Println("b2:", i)
            }(i)
        }
    }()
    fmt.Println()
    func() {
        for i := 0; i < 3; i++ {
            i := i // 里面的i用外面的i来赋值
            defer func() {
                fmt.Println("b3:", i)
            }()
        }
    }()
}

上面的是defer function的参数估值时机,第一个b会打印3个3,因为出栈的时候考察i,已经都是3了。

下面看一下goroutine function的参数评估时机。

package main

import "fmt"
import "time"

func main() {
    var a = 123
    go func(x int) {
        time.Sleep(time.Second)
        fmt.Println(x, a) // 123 789
    }(a)

    a = 789

    time.Sleep(2 * time.Second) // 用sleep来同步协程不太好的
}

会打印123,789呢,就是说在go开启新协程的时候将a=123送入了这个func,但是在执行里面的语句的时候,a是直到fmt.Println调用的时候才evaluated(上面的一段说的),所以a是父协程的资源,执行到a=789了。

Panic and Recover

前面说过了嗷,defer存在的意义就是为了panic和recover服务

go没有异常抛出和捕获!error handling是go里面用的。和throw/catch机制很像的就是这个panic/recover

调用panic函数创建一个慌张。。。。可以让当前的goroutine进入panicking status,当然这个panic只能在当前协程中存在。

panicking是另一种让函数return的方法。在一个函数调用中产生了一个panic会让函数立刻返回并进入exiting phase,然后defer-call stack就会执行咯。

在deferred call中调用recover函数,就可以将前面生出来的活着的panic给去掉!然后当前的goroutine就会重新进入calm的状态。。。go的单词用的真是活灵活现。

如果一个慌张的goroutine没有recover就退出,那么整个program就carsh了。

func panic(v interface{}) // 接收一个interface
func recover() interface{} // 返回一个interface

interface类型之后会介绍

Here, we just need to know that the blank interface type interface{} can be viewed as the any type or the Object type in many other languages. In other words, we can pass a value of any type to a panic function call.

就是说recover的返回值会喂给panic,吃饱了就不panic了。

遇到错误就会产生panic,程序就慌了,一慌就不执行了,就想回家return,但是回去的时候还要把延迟的工作完成,如果延迟的工作里面能解决recover,就不慌了。

看一下产生panic的一个情况 0做除数,更多panic后面..大多数都是逻辑错误

package main

import "fmt"

func main() {
    
    defer func() {
        rv := recover()
        fmt.Println("recover: ", rv)
    }()
    a, b := 1, 0
    _ = a / b // 这里慌了 return
    fmt.Println("done")
}

Some Fatal Errors Are Not Panics and They Are Unrecoverable

For the standard Go compiler, some fatal errors, such as stack overflow and out of memory are not recoverable. Once they occur, program will crash.


Go type system

下一大章节!go的系统,上面的go大体代码都看的懂了。

Go Type System Overview

介绍所有的类型和概念。

Concept: Basic Types

稍微回顾一下所有的内置类型,自行脑补吧。反正byte alias of uint8,rune alias of int32

Concept: Composite Types

组合类型?就是其内部是基础类型的组合。一会按着顺序讲

  • pointer types和C的指针很像
  • struct types和C的结构体很像
  • function types函数是Go的第一阶级的类型哦
  • container:
    • array 定长
    • slice 动态长度和动态容量
    • map 和字典差不多吧 哈希的
  • channel types用来在goroutine之间同步数据的
  • interface types是reflection和polymorphism(多态)的关键???

其他未定义的组合类型就是把组合类型和基础类型搞在一起,看一些例子

// Assume T is an arbitrary type and Tkey is
// a type supporting comparison (== and !=).

*T         // a pointer type
[5]T       // an array type
[]T        // a slice type
map[Tkey]T // a map type key的类型是Tkey value的类型是T

// a struct type
struct {
    name string
    age  int
}

// a function type
func(int) (bool, string)

// an interface type
interface {
    Method0(string) int
    Method1() (int, bool)
}

// some channel types
chan T
chan<- T
<-chan T

Fact: Kinds of Types

讲一个fact?上面讲的类型都是go的26个类型的一个。还有一个unsafe指针,看go的标准库吧。

Syntax: Type Definitions

定义类型的语法,用types关键字

type newType sourceType

多个定义可以用括号放一起。

But please note that, type names declared at package level can't be init. (This is the same for the following introduced type alias names.)就是在package级的类型定义不能被初始化。

说是有两种方式,先看第一种

type (
    mstr string
    age  uint8
)
type intPtr *int
type book struct {
    author, title string
    pages         int
}

// 将这样的函数类型重命名为convertor
type convertor func(in0 int, in1 bool) (out0 int, out1 string)

注意事项:

  • 新定义的类型和对应的源类型是两个不同的类型
  • 新类型和源类型的underlying类型是一样的,两个类型的值可以互相转换
  • 在函数体内可以定义类型

Syntax: Type Alias Declarations

从go1.9之后的新的type alias declaration

上面也说到了byte类型和uint8就是alias关系,and rune和int32。。。

=来建立alias

type (
    Name = string
    Age  = int
)

type table = map[string]int
type Table = map[Name]Age  // 注意哦这个alias是可以被exported的因为identifier是大写

怎么想到了cpp里面也是两种,一种c的typedef,一种cpp的using xxx = int;

这种alias的区别,就是它是alias了一个新名字,而不是建立了一个新类型。记住了哦!

Concept: Defined Types vs. Non-Defined Types

type A []string  // A是defined的了
type B = A
type C = []string  // 都是non-defined []string是non-defined 然后C是他的alias

Concept: Named Types vs. Unnamed Types

就是在go1.9以前才有的named type和unnamed type,之后呢就有了alias,会造成混乱。go101给出了以下规则:

  • An alias will never be called as a type, though we may say it denotes/represents a type.
  • The terminology named type is viewed as an exact equivalence of defined type. (And unnamed type exactly means non-defined type.) In other words, when it says "a type alias T is a named type", it actually means the type represented by the alias T is a named type. If T represents an unnamed type, we should never say T is a named type, even if the alias T itself has a name.就是在alias的时候,alias要看源类型是否是named
  • When we mention a type name, it might be the name of a defined type or the name of a type alias.

Concept: Underlying Types

躺在下面的类型,每个类型都有一个underlying type

  • 内置类型的underlying就是自己
  • for the Pointer type defined in the unsafe standard code package, its underlying type is itself. (At least we can think so. In fact, the underlying type of the unsafe.Pointer type is not well documented. We can also think the underlying type is *T, where T represents an arbitrary type.)
  • the underlying type of a non-defined type, which must be a composite type, is itself.
  • 新定义的类型和源类型share一样的underlying type

In Go,

  • types whose underlying types are bool are called boolean types;
  • types whose underlying types are any of the built-in integer types are called integer types;
  • types whose underlying types are either float32 or float64 are called floating-point types;
  • types whose underlying types are either complex64 or complex128 are called complex types;
  • integer, floating-point and complex types are also called numeric types;
  • types whose underlying types are string are called string types.

这个躺在下面的类型对于值转换,赋值和比较是很重要的。

Concept: Values

value的概念是一个type的instance,每个类型都有一个零值,可以看成是一个类型的默认值。之前声明的nilidentifier可以用来表示slice,map,function,channel,pointer(含unsafe),interface。nil之后还要看看。

讲一讲剩下的两个literals,literal就是表示值的一个方法

Function literals,函数literal就是来表示函数的value的。

Composite literals 用来表示结构体类型和容器类型的。

pointer,channel,interface是没有literals的。

有点潦草,毕竟是概述,literal还是有点晕乎。

Concept: Value Parts

值的part,总所周知value都是存在内存里面的,所谓的part就是在内存中连续的segment,有些值是有不同的parts叫做indirect的parts,好比cpp的链表,通过在direct的地方(就是直接地址)用pointer来reference到。这也是go101中说法,go官方没这么说。

Concept: Value Sizes

值存在内存中的字节数。可以用Sizeof函数来查看

Go官方也并没有说非数值类型的大小

Concept: Base Type of a Pointer Type

Concept: Fields of a Struct Type

结构体中的每个成员叫做field of the struct type

Concept: Signature of Function Types

函数签名,是函数类型的输入和输出list

函数名和body不是函数签名的部分

Concept: Method and Method Set of a Type

方法,更好的解释是成员函数。

Concept: Dynamic Type and Dynamic Value of an Interface Value

Each interface value can box a non-interface value in it. The value boxed in an interface value is called the dynamic value of the interface value.

The type of the dynamic value is called the dynamic type of the interface value.

An interface value boxing nothing is a zero interface value.

A zero interface value has neither a dynamic value nor a dynamic type.

晕了,后面详细学一下

An interface type can specify zero or several methods, which form the method set of the interface type.

interface类型的成员函数(method)

If the method set of a type, which is either an interface type or a non-interface type, is the super set of the method set of an interface type, we say the type implements the interface type.TODO不懂。。

具体还是要搞明白interface是啥

Concept: Concrete Value and Concrete Type of a Value

具体值和具体类型,非interface类型的,就是其本身

A zero interface value has neither concrete type nor concrete value. For a non-zero interface value, its concrete value is its dynamic value and its concrete type is its dynamic type.

Concept: Container Types

容器,都有length

Concept: Key Type of a Map Type

map[Tkey]T

Concept: Element Type of a Container Type

容器的元素类型都是一样的!

[N]Tmap[Tkey]Tchan Torchan<-Tor<-chan T他们的类型都是T

Concept: Directions of Channel Types

Channel values can be viewed as synchronized first-in-first-out (FIFO) queues. Channel types and values have directions.通道是同步用的FIFO队列,有方向,所以有箭头这玩意。。

  • 双向channel,可接收和送,chan T作为literal表示
  • send-only channel,chan<- T
  • receive-only channel,<-chan T

Fact: Types Which Support or Don't Support Comparisons

那些是不能比较的

  • slice types
  • map types
  • function types
  • any struct type with a field whose type is incomparable and any array type which element type is incomparable

Fact: Object-Oriented Programming in Go

Go is not a full-featured object-oriented programming language, but Go really supports some object-oriented programming styles.

Fact: Generics in Go

泛型编程还是仅限内置类型,自定义的还不行,还在go的draft phase阶段(go1.13 now)


Pointers in Go

go虽然吸收了很多其他语言的特性,但是大部分还是被视为是C家族的语言。也有指针,用法大多相似,也有不同。这篇详谈。

Memory Addresses

内存地址其实就是一个offset from the 内存始地址。内存地址被存为一个无符号native(integer),4字节on 32-bit architectures,8字节在64位上。所以最大内存是2的32次字节,4gb,和2的34次方GB在64位。

通常都是用hex integer,十六进制表示

Value Addresses

The address of a value means the start address of the memory segment occupied by the direct part of the value.直接value地址

What are Pointers in Go

相同的不赘述了,不同的是为了安全考虑,有一些限制。。后面会介绍。。。

Go Pointer Types and Values

一个non-defined指针可以用*T表示,T是任意类型。

We can declare defined pointer types, but generally, it’s not recommended to use defined pointer types, for non-defined pointer types have better readabilities. 问题是哪里有defined pointer,好像还没遇到过。

underlying type是*T,还有一个base type定义为T

如果有两个non-defined指针类型有一样的base type,那么他们就是一个type的。注意这个T也可以是*T1

*int  // A non-defined pointer type whose base type is int.
**int // A non-defined pointer type whose base type is *int.

// Ptr is a defined pointer type whose base type is int.
type Ptr *int   // 出现了! defined ptr
// PP is a defined pointer type whose base type is Ptr.
type PP *Ptr

指针类型的0值是predeclarednil,也就是没有存地址的指针,cpp11的nullptr

base type是T,则只能存T类型的值的地址

About "Reference"

In Go 101, the word "reference" indicates a relation.一个指针存了另一个值的地址,那么这个指针就可以说是(directly) references 另一个值。当然了,一个另一个值有至少一个reference。

当然也可以说是指向(points)那个值

how to get a pointer value and what are addressable values?

如何定义一个ptr呢,以及什么值是有地址的(addressable)

有两种方法获得一个non-nil pointer

  1. 也有一个new,但他是个function来为一个类型的值分配空间,返回的是一个指针*T,地址所指向的值是0值。

  2. 同样也可以用取地址符号&

所谓的addressable就是在内存中有房间的值,变量。constant,function calls和explicit conversion result都是unaddressable的

When a variable is declared, Go runtime will allocate a piece of memory for the variable. The starting address of that piece of memory is the address of the variable. 这句话好好品一品,变量所需要的地址是一片memory,这一片的起始地址才是他的地址。

Pointer dereference

和C用法一样。*pdereference,是取地址的反操作。

Dereferencing a nil pointer 会造成程序的慌乱哦

Why do we need pointers?

Return Pointers of Local Variables Is Safe in Go

写cpp的时候返回值是一个指针,且这个指针是在函数体内被new出来的,那么在用完它之后必须要delete释放空间。

但是在go里面,有garbage recycle,所以很safe的可以return pointer

Restrictions on Pointers in Go

go为了安全所做的限制

  • 不能++或者算术运算like p++,p-2,如果p指向的是数值类型,那么编译器会把*p++合法看成(*p)++,就是说&*的优先级高于++,--

  • 一个ptr不能被转换成任意类型的ptr,就不能像cpp一样(double*)por(void*)p,除非满足一下两个情况的任意一个:

    1. 两个指针类型的underlying type是一样的(identical!)不考虑结构体,尤其是在两个指针类型都是non-defined的时候他们的underlying type是一样的(考虑结构体的tag 后面会讲),那么可以隐式转换。
    2. Type T1 and T2 are both non-defined pointer types and the underlying types of their base types are identical (ignoring struct tags).
    type myInt int64
    type Ta *int64
    type Tb *myInt
    ptrMy := new(myInt)
    var ptrA Ta
    var ptrB Tb
    ptrB = (*myInt)((*int64)(ptrA)) // Ta 到 Tb要经过中间商*int64
    ptrA = (*int64)(ptrMy)
  • 一个指针不能和其他任意类型的指针比较,除非:

    1. 是相同的类型
    2. 可以发生转换,换句话说就是他们的underlying类型是必须一样的而且他们之中至少一个是undefined type
    3. 只有一个是nil,也就是判断是否是空指针的时候
  • 不能赋值不同类型的指针变量,但可以赋值可比较的指针

It's Possible to Break the Go Pointer Restrictions

As the start of this article has mentioned, the mechanisms (specifically, the unsafe.Pointer type) provided by the unsafe standard package(到了unsafe的那篇文章会讲) can be used to break the restrictions made for pointers in Go. The unsafe.Pointer type is like the void* in C. In general the unsafe ways are not recommended to use.


Struct

Struct Types and Struct Type Literals

首先要直到怎么声明

non-defined struct如下

struct {
    title  string
    author string
    pages  int // 这都叫做field 其他的文章也说成是member
    // title, author string 当然也可以这样啦
}

struct类型的size就是所有的field类型的size之和加上一些padding字节,这还是很有意思的,应该就是为了补足到2的多少次方大小。之后会将memory

没有field的struct的size为0

还可以给每个field绑定一个tag!是optional的,来看看

struct {
    Title  string `json:"title"`
    Author string `json:"author,omitempty" myfmt:"Author"`
    Pages  int    `json:"pages,omitempty"`
}

Generally, the tag of a struct field should be a collection of key-value pairs. Tag应该是一个键值对,然后值是string,我们可以用reflection方法检查field的tag信息,之后学。。。

tag存在的作用和所需要的应用场景有关。上面那个例子,tag可以帮助encoding/json标准库的函数去检查json, The functions in the encoding/json standard package will only encode and decode the exported struct fields, which is why the first letters of the field names in the above example are all upper cased.

将tag当做是comment,可不太聪明

Raw string literals (...) are used more popular than interpreted string literals ("...") for field tags in practice.

Go的struct不支持unions,回顾一下c,unions就是一个成员的类型可以有好多个,但他的出现只会用到其中的一个类型,内存共享,大概就是这个意思。

union var {
    char c[4];
    int i;
} // 比struct好,struct不管用不用,全分配内存
// union分配的内存大小是两个类型size的最小公倍数
// 共用互斥。

上面的struct都是non-defined和匿名的,用的时候还是要type一下。

Only exported fields of struct types shown up in a package can be used in other packages by importing the package. We can view non-exported struct fields as private/protected member variables.就是说想要被exported就一定要大写首字母,小写的就可以看成是私有成员(oop思想)

field写的顺序是要紧的,两个non-defined结构体他们的field声明顺序是一样的才是一样的。Two field declarations are identical only if their respective names, their respective types and their respective tags are all identical.

注意,不同package中的两个non-exported结构体field名字被视作不一样的两个名字。

A struct type can't have a field of the struct type itself, neither directly nor recursively.

Struct Value Literals and Struct Value Manipulations

结构体变量用T{...}去初始化,T必须是一个结构体,叫做composite literal(说中文就是这个T是一个合成的表示形式)

st := struct {
    age  uint8
    name string
}{12, "hh"} // 直接给值必须是按顺序给的

让这个struct是0值就不给值直接{}(最常用)

可以用键值对的形式

type noval struct {
    title  string `json: "title, omitempty"`
    author string `json: "author"`
}
n1 := noval{title: "ss"}
n2 := noval{} // 确实的键值对就会直接赋0值

如果一个结构体是外面的package导入的,最好就是用键值对,因为如果在外面的package中被增加一个field的话,那么可以避免不兼容。

和c差不多,用.去select field,叫做选择器表达式!

如果写non-defined结构体,注意最后的coma。。。

var _ = Book {
    author: "Tapir",
    pages: 256,
    title: "Go 101", // here, the "," must be present
}

// The last "," in the following line is optional.
var _ = Book{author: "Tapir", pages: 256, title: "Go 101",}

About Struct Value Assignments

结构体之间的赋值,就是每个field赋值,注意指针也是直接赋值的。。

Struct Field Addressability

这个还是很有意思的吧,看看首地址到底指的是谁!

结构体的field的addressability和struct保持一致。

unaddressable的结构体她的field不能被改变

All composite literals, including struct composite literals are unaddressable.看个例子吧。组合的名字就没地址了

package main

import "fmt"

func main() {
    type Book struct {
        Pages int
    }
    var book = Book{} // book is addressable
    p := &book.Pages  // selector的优先级是大于取地址的
    *p = 123
    fmt.Println(book) // {123}

    // The following two lines fail to compile, for
    // Book{} is unaddressable, so is Book{}.Pages.
    /*
    Book{}.Pages = 123
    p = &(Book{}.Pages) // <=> p = &(Book{}.Pages)
    */
}

Composite Literals Are Unaddressable But Can Take Addresses

Go有一个语法糖。。可以取composite literals的地址

A syntactic sugar is an exception in syntax to make programming convenient. 所谓语法糖,就是带来便利的,小糖果哦......

// noval{}.author = "ss" // 相当于是一个临时变量
p := &noval{} // 相当于先有个tmp := noval{} 然后在p := &tmp
p.author = "ssss" 
// p1 := &noval{}.ptr // 不行
fmt.Println(*p)

In Selectors, Struct Pointers Can Be Used as Struct Values

还好,是这样,不然上面的例子就错了,go的指针没有->这个,因为被channel用掉了,所以还是用.去访问成员

About Struct Value Comparisons

Most struct types are comparable types, except the ones who have fields of incomparable types.这句话看似真是废话。。。

比较相同的时候是每个field都相同才相同

About Struct Value Conversions

Anonymous Struct Types Can Be Used in Field Declarations

就是一个struct的field也可以是struct

var aBook = struct {
    // The type of the author field is
    // an anonymous struct type.
    author struct {
        firstName, lastName string
        gender              bool
    }
    title string
    pages int
}{ // 这里都是初始化 只能这样来
    author: struct { // an anonymous struct type
        firstName, lastName string
        gender              bool
    }{
        firstName: "Mark",
        lastName: "Twain",
    },
    title: "The Million Pound Note",
    pages: 96,
}

More About Struct Types

还有两篇拓展,type embedding和memory layouts


Value Parts

为了之后的一些类型能更好的理解,我们先学一下这个值part的部分,因为后面涉及到不连续内存的问题了。

Two Categories of Go Types

类C语言,之前的两个类型的内存结构和c差不多

但是咧,go语言的几个类型的内存结构并不是完全透明的,c是。 Each C value in memory occupies (TODO)one memory block (one continuous memory segment). However, a value of some kinds of Go types may often be hosted on more than one memory blocks.

一个不止拥有一个内存block的值(value part)由一个direct part统一组成,并且其他的(underlying parts)parts are referenced by that 直接value part

所以这个标题就想介绍有两种存的方式,一个是只hosted on一个内存block的,还有一种是有multiple memory blocks的。

Types whose values each is only hosted on one single memory block Types whose values each may be hosted on multiple memory blocks
solo direct value part direct part --> underlying part
boolean, numeric, pointer, unsafe pointer, struct, array slice, map, channel, function, interface, string

注意:

  • interface和string是否有underlying parts取决于编译器,标准go compiler下,他们都可以有
  • function值是否有underlying parts很难去证实,在go101中认为是有的

第二种类型给go编程带来便利因为封装了很多细节,不同的go编译器对他们的实现方式不一样。

同时他们也不是很基础的类型,我们甚至可以自己造轮子。。enjoyable and productive

Two Kinds of Pointer Types in Go

前面讲的pointer 都是type-safe的,也有type-unsafe pointer类型,在unsafe标准库中,unsafe.Pointer像c的void*

Below, we call a struct type with fields of pointer types as a pointer wrapper type, and call a type whose values may contains (either directly or indirectly) pointers a pointer holder type.

(Possible) Internal Definitions of the Types in the Second Category

就是内部实现的时候定义了好多好多的(中间)类型。TODO全部学完类型了可以回过头看看。

Underlying Value Parts Are Not Copied in Value Assignments

浅拷贝?

Now we have learned that the internal definitions of the types in the second category are pointer holder (pointer or pointer wrapper) types. Knowing this is very helpful to understand value copy behaviors in Go.

所有的go的赋值过程都是浅拷贝!

只有direct part of source value才被copy,所以强调了pointer holder

In fact, the above descriptions are not 100% correct in theory, for strings and interfaces. The official Go FAQ says the underlying dynamic value part of an interface value should be copied as well when the interface value is copied. However, as the dynamic value of an interface value is read only, the standard Go compiler/runtime doesn't copy the underlying dynamic value parts in copying interface values. This can be viewed as a compiler optimization. The same situation is for string values and the same optimization (made by the standard Go compiler/runtime) is made for copying string values. So, for the standard Go compiler/runtime, the descriptions in the last section are 100% correct, for values of any type.

上面这段话就是说像interface(尚未习得)和string的只读和不变的数据在拷贝的时候确实不用深拷贝,浅拷贝就够了,是一种编译器优化,但是如果当string要改变的时候再深copy出来,改变。想到了之前学cpp的oop的string类的实现,go其实也是这个道理。

由于躺在下面的part不能成为值的专属,那么unsafe.Sizeof这个函数其实是不能记录的。一个string的size一直都是16


猜你喜欢

转载自www.cnblogs.com/coyote-waltz/p/12416203.html