Golang学习笔记


运算符

  • 如何定义一个数组?

    • 推导方式下的:

      • 先用var声明变量的名字

      • 然后在等于赋值的时候先用[…]表示是一个数组类型

      • 然后紧跟着写出数组中元素的类型,比如int

      • 再然后就是数组的内容,内容是和java一样,用花括号去包裹的

      • 样例:

        • var arr=[...]int{1,1,2,3,3,4,4,5,6,5,6}
          
  • 变量只是var了一下,然后在下面继续使用的时候是会报错的,没办法进行推导,也就是说推导方式定义变量的话,这个推导一定要和var的时候一起用

    • 比如:
      • 先var flag,然后在下面循环的时候写了一个flag=true,是会报错的
      • 但是我写在一起,也就是var flag = true,这样就是正确的
      • 或者在var的时候就把类型确定好,var flag bool,然后在for循环里面赋值是可以的
    • 也就是说这个推导没有那么智能,只有和var写在一行才能推导,因为var的时候必须要确定下来这个类型变量所需要的空间大小
  • for循环书写的方式:

    • 用下标去遍历数组的时候,int i=0是可以直接简化写成i:=0的

    • 用foreach的方式去遍历的话:

      • java中是item类型 item:集合

      • go中是item:=range 集合的方式,理解起来就是定义了一个循环遍历item,然后item的循环值范围就是集合的数据内容,用range去表示这个范围

      • 需要注意的是,这边添加了一个“—”匿名变量

        • 疑问这个匿名变量是干嘛的,为什么会出现?
      • 示例:

        • for _, num := range arr {
          
  • go中应该是没有对象的概念的,所以如何获取数组长度呢:

    • 就是用len方法
      • 疑问:len方法是哪里的?是包里面的吗?如果是包里面的,那为什么没有加包名?
  • fmt里面的Println的使用方法和java中的println不太一样

    • java中是直接用加号做字符串的拼接
    • go中是用逗号表示字符串的拼接
  • go中函数的定义方式:

    • func表明我要定义的是一个函数

    • 然后跟着函数名,后面括号写上参数

    • 参数是如何写的呢?

      • 因为传入的是参数,所以不用加var,var就是定义的时候用
      • 还是老规矩,先写参数名,然后后面写类型
      • 如果是数组类型,这个类型怎么写呢?
        • 就是先写[]表明是数组,然后后面跟着数组中的元素类型,比如int
    • 再后面就是函数的返回值类型

      • 如果没有返回值类型的话,就直接不写就行
    • 样例:

      • func fundUnique(array []int) int
        

流程控制

  • if条件判断

    • else或者else if必须要和上一个分支结束的右花括号在同一行

    • 在 if 判断之前添加一个执行语句,然后再写正常的if判断

      • 这个执行语句因为是语句,所以是以分号结尾的,因为和if的判断在同一行,所以没有办法省略这个分号

      • 样例:

        • if score := 65; score >= 90 {
          
      • 疑问:这种写法和正常写法应该是有点区别的吧,这个变量的作用域是不是不一样啊

  • for循环

    • 最普通的for循环是有三个部分的,如果省略了初始语句的话,一定要加分号
      • 但是如果省略了初始语句和每次循环结束后的语句的话,也就是只剩下了循环的判断语句,那么这个时候其左右就不用加分号了
      • 感觉这边的for就很类似于while的作用了,也就是说for后面只有一个式子的时候,就相当于java中的while作用
    • 那么如何表达无限的循环呢
      • 在java中是while(true),那么因为while在go中相当于if后面跟着一个式子,所以还是先写一个for
      • 然后这边的括号肯定是省略了
      • 再然后这个true其实也可以省略,也就是说for后面什么式子都没有再跟“{”的话,就可以表示无限循环
    • 循环除了可以用break和return和goto去强制退出外,还可以用panic去强制退出
      • 疑问:这个panic是啥意思啊
  • switch case

    • switch只能有一个default分支

    • 一个case后面可以跟多种可能值,这些可能值之间用逗号分隔

      • 也就是说,多个值可能对应着同一个处理逻辑
    • 当switch后面不加需要判断的变量直接“{”时,在下面的case中可以写表达式

      • 感觉这种方式和if…else if…else…更像了

      • 样例:

        • switch {
          	case age < 25:
          		fmt.Println("好好学习吧")
          	case age > 25 && age < 35:
          		fmt.Println("好好工作吧")
          	case age > 60:
          		fmt.Println("好好享受吧")
          	default:
          		fmt.Println("活着真好")
          	}
          
      • 我发现当本应该跟着式子的关键字后面什么都不加时,则表示默认的true

    • fallthrough起什么作用呢

      • 会执行这个fallthrough下一个并列的case的逻辑,甚至不去判断这下一个case表达式的内容是否为真
      • 但是只是下一个而已,不是一直持续下去
    • go中的switch…case…比较方便的是会默认是添加了break的,不用自己手动添加break

  • goto

    • 如何使用呢
      • 先是打标签,标签名:的形式,后面是标签所标识的代码
      • 然后goto 标签名,就可以直接跳到那个标签的代码开始执行
  • break

    • break语句还可以在语句后面添加标签,表示退出某个标签对应的代码块
      • 但是这边的标签必须是定义在for、switch或者select的代码块上的
      • 因为break主要就是跳出循环,当标签定义的就是循环或者类似循环的结构时,break就知道自己应该跳出的是哪个循环了
  • continue

    • continue的后面也是可以加标签的
    • continue后面加标签表示的是开始进行标签所标识的循环的下一次循环
  • 九九乘法表a*b,b每一行都不一样,是标识不同行的,a是从1一直到b的


数组

  • 数组的长度是不可以变化的
  • 如何声明一个固定长度的数组呢
    • 在[]里面写数组的长度
      • 示例:var a [3]int
    • 和C语言一样,这里的数组长度必须是个常量,而java中的数组长度是可以写变量的,这是一点不同
    • 数组长度不一样的数组都不是同一种类型,就是说[5]int和[10]int不是一种类型的变量,那么他们就不可以进行互相赋值等操作,毕竟不同类型的变量怎么能互相赋值呢,除非强制转换
  • 这个panic看样子像是报错的意思
  • 之前遇到过[…]int{1,2,4}这种情况,其实这个省略号就是让编译器根据初始值的个数自行推断数组的长度
  • 那么[3]int{1,2}这种会出现什么结果呢
    • 规定了是3个元素的数组,却只赋了两个元素,那么最后一个元素在是int类型的情况下,就会默认是0
  • %T这种格式化输出的方式可以打印出变量的类型
    • 因为数组的长度算在了数组类型中,所以对数组变量进行%T的时候,会显示出数组的长度
  • 数组还可以为特定的位置赋值(在初始化的时候)
    • 形式就是 索引值:赋的值
    • 示例:a := [...]int{1: 1, 3: 5}
  • 感觉一个变量如果已经想好了初值那就用“:=”的形式
    • 如果没有想好初值,就先var去定义
  • 解密了,找到了for range这种循环遍历方式里面的匿名变量是干嘛的了,应该是计数的,需要计数的时候就不用匿名变量去接,不需要的时候再用
    • 那么这个计数是从0还是1开始的呢
      • 测试过了,这个下标和数组下标一样,是从0开始的
  • 多维数组是 [行数][列数]数组类型{数组元素} 的定义方式
    • 其中行数是可以用“…“来让编译器自己去推导的,但是列数却不行
  • 和java和C语言不同的是,数组在作为参数传递的时候属于值传递,也就是说,在函数里面改变的是数组的副本,原本数组的内容并没有被改变
  • 指针数组和数组指针的区别
    • 指针数组还是数组,里面放的元素类型是指针,那么形式就是[元素个数]*指针指向元素类型
    • 数组指针还是指针,指向的元素类型是数组,那么形式就是*[元素个数]元素类型
  • 之前说过这个数组的长度也是算在了数组的类型里面,那么这边在数组作为函数参数传递的时候就有个坑
    • 数组定义的时候长度就确定下来了(即使用的是[…],数组长度也是一个确定的值,而且这个值是不会发生改变的),那么我们在函数定义的时候需要确定参数类型时,如果参数是数组,那么也一定要指明确定长度的数组,不然就会出现问题
    • 疑问:那我如何做到让一个函数可以处理各种长度的数组呢
      • 有办法了,就是在定义数组的时候就不指定它的长度,也就是说不指定长度也是可以定义好数组的,像这样arr := []int{1,3,5,7,8},然后在函数中写数组类型参数时,也不写死数组的长度
      • 纠正:后面才知道,原来这个是切片类型,已经不是数组类型了

切片

  • 切片的英文是slice

  • 切片顾名思义,切片相当于一个分割片状物,它是可以滑动的,特性就是长度可以自由地改变

  • 切片是为了解决数组的两大问题而出现的

    • 数组长度不可变
    • 数组是值类型,作为函数参数传递时改变的是副本而不是数组本身
  • 感觉切片就很像是java中的ArrayList类型,这个类型也是引用类型,并且长度可以自由地改变

  • 如何声明一个切片类型呢

    • 切片类型和数组类型的声明实在是太像了,其实就是去掉数组类型声明时指定的数组长度或者“…”
  • 引用类型之间是不能用等于号去直接进行比较的,因为等于号比较的是地址,这个要视情况而定

    • 引用类型是可以和nil去用等于号比较的,因为nil本身就表示一些引用类型的0值
  • go中有一个相当于java中null值的存在,叫做nil

    • nil在英文中是“无,零”的意思,在go中表示一些引用类型的0值
  • 切片只要加了{}就不是nil值了,也就是说{}表示初始化

    • 在数组中{}也表示初始化
  • 切片有自己的长度和容量

    • 长度我知道,用len方法获取,表示切片中元素的个数
    • 那么容量是什么意思的
      • 容量的英文是capacity,所以这里获取切片容量的函数是cap
      • 疑问:那么容量所表示的含义是什么呢?
  • 切片表达式

    • 切片表达式就是说写一个表达式,得到的结果最后是一个切片
    • 两种类型的切片表达式
      • 第一种是比较简单的,直接从数组中获取部分连续的值作为切片的内容
        • 写法就是 数组变量名[low,high]
        • 疑问:容量等于得到的切片的底层数组的容量是什么意思,从实验结果来看这个切片的容量并不是来源数组的容量,而且随着low和high的不同,这个切片容量也会不一样
        • %v是打印结构体的意思
          • 实验发现,可以输出数组格式,数字也可以输出,感觉基本上什么都可以输出的样子
          • 关于更多的格式化输出内容见链接:http://www.manongjc.com/detail/23-mswkdvafgiwivdk.html
        • 这边数组换成字符串也是可以的
        • 切片也可以再进行切片表达式,但是high的要求有点不太一样
          • high的上限是源头切片的容量而不是长度
          • 疑问:啊,现在问题更大了,这个容量到底是个啥啊
      • 完整切片表达式
        • 这里完整的含义就是指多写一个数,和容量的值有关
        • 以前貌似多加一个数和步长有关什么的,但是在go中不是这样,而是表示容量
        • 书写形式就是 数组变量名[low:high:max],这里的max就是容量值-low值,要想知道真正的容量值是多少,就用max-low就行
          • 也就是说这个容量的值是 小于等于 max值的
          • 但是要注意的是,这个max虽然比最后结果切片的容量大,但是却小于源头数组等的容量
          • 同时这个max肯定还要比high大
        • 还有一点与简单切片表达式不一样的是,这个表达式不可以作用于字符串,其他都是正常
          • 疑问:为什么不可以作用于字符串呢?
        • 还有一点,在这个表达式中,只有low是可以省略的,其他都不行
          • 这边的省略应该是说可以不写值,但是分隔值的冒号还是要写的
  • 上面已经有三种方式定义切片了

    • 第一种是直接定义,就是数组不加长度即可
    • 第二种就是借助数组、字符串、切片等的切片表达式
    • 第三种是使用内置的make函数
      • 上面是内置呢
        • 顾名思义就是内部存在的,不需要引入包什么的就可以直接用的
      • 这个make函数主要是为了弥补这个第一种直接定义的方式的缺陷
        • 第一种直接定义,因为不写数组长度,所以切片初始的长度无法定义,一旦定义了就变成了数组而不是切片
        • 同时也没有办法去设置我们想让它具有的容量
        • 因此用make函数就比第一种方式多了两个属性的设置,切片初始长度和容量
        • 形式就是 make([]切片元素类型,切片长度,切片容量)
        • 解密了,大致明白了容量和长度的区别
          • 容量就是说内部存储空间实际给切片分配出来的大小
          • 长度就是实际使用的大小
        • 在make的定义方式中,这个实际使用的控件中初始出来的值都是0
        • 很重要的一点是,这种方式相当于完成了定义和初始化两步
          • 那么这个就不等于nil了
          • 这边值得注意的是,什么情况下切片才等于nil呢
            • 第一种定义方式中,没有{},就是nil
            • 有了容量的时候切片就不是nil(容量有了存储空间就有了,怎么能说是nil)
      • 偶然发现,这个函数的第二个参数表示长度和第三个参数表示容量都是可以省略的
      • 很重要的一点是make出来的切片即使两个参数都省略了,也不是nil,也是初始化过的,因为这个省略不是不设置,而是默认设置成了0
  • 切片的本质(非常帮助理解)

    • 实际上切片存储的内容是非常少的,只有三个
      • 指向底层数组的指针(可以理解为切片的开始位置)
      • 切片的长度(可以理解为切片从指针开始后面多少位元素是我们需要的)
      • 切片的容量(可以理解为切片从指针开始后面可以最大扩大什么地步)
    • 解密了,完整表达式中第三个值max的含义
      • max表示的是下标,就是切片所拥有的最大扩容到的下标-1,所以定义出来的切片的容量是这个max-low
      • 所以这个max并不是指最大容量,而是指一个最大下标,并且并不包括这个最大下标
    • 所以可以发现切片只是比数组多了两个属性而已,一个长度,一个容量
  • 那么如何判断切片是否为空呢

    • 推荐使用len(切片变量名)==0
    • 因为如果用nil可能会出现当切片的长度明明是0了,表示切片已经是空了,但是却判断出切片不是nil这种不恰当的感觉
    • 而len==0却是包含了两种情况,切片为nil和切片不为nil但长度为0
    • 所以一定要用len是否为0来判断切片是否为空
    • 破防了,家人们,突然发现,因为任何一个变量不可能出现没有初始化使用的情况,这个是go语言规定的,所以这个切片一定不会是nil,那么不管切片是否为空,==nil的结果永远都是false,那肯定要用len去判断是否为空
  • 切片虽然不能直接用等于号判断是否相等,但是可以用等于号去赋值

    • 这个赋值是一个浅层的赋值,我们知道切片是有一个底层数组的,这个浅层的赋值就是共享底层数组,那么其中一个改变了底层数组,那么另一个自然也就会被影响
    • 这个真的是一个奇怪的现象,切片可以用赋值语句,却不可以用等号判断是否相等
  • 内置函数的一个点,就是内置函数里面必须有一个参数是函数操作的对象,毕竟和java不一样,不是面向对象(面向对象的话,可以用对象点的方式去知道操作的对象是谁)

  • 之前就说切片的长度是可以变的,那么怎么变呢

    • 添加元素用的是append内建函数

      • 第一个参数肯定是切片本身
      • 然后跟着要添加的元素,一个或者多个
      • 也可以跟着切片,也就是说在切片后面加切片
        • 那么如何表示加切片呢,只追一个切片变量是没有用的,需要在这个切片变量后面加一个“…”表示这个变量是切片
      • 这个函数很神奇的地方是,只定义没有初始化的nil切片也是可以直接使用的(这个也是和java中的一个很大的不同)
      • 添加元素时会遇到一个问题,就是切片的容量可能不够用了(这实际上是底层数组不够用了),那么这个时候其实还是可以进行append添加元素的
        • 这时append实际上做的操作是更换底层数组,换个更大的,这个操作叫做扩容
        • 可以发现这个默认的容量扩容是按照2的次方次进行扩容的(前提是没有指定这个切片的容量的情况下,切片表达式的方式去定义切片时这个容量其实也算是指定了的,因为它的底层数组被确定了下来)
        • 疑问:如果刚开始切片的容量指定了,那么这个扩容是按照上面策略扩容的呢
          • 看那个意思,好像是不管怎么样都是旧容量的两倍(那么后面容量很大的时候扩容扩的也会比较大欸)
            • 解密了,家人们,这个两倍不是一直这样下去的,会有一个临界值,貌似是这个样子的,这个不确定
        • 补充一下如何看slice切片相关的源码:在go的sdk文件夹中,目录是src/runtime/slice.go
          • 上面这个源码里面可以看到一些切片的扩容策略,这里面提到了一个内容,就是新申请的容量,这个是什么意思,是指我们手动去申请的容量吗,还是说调用append函数式自动传入的容量呢
          • 关于上面的这个扩容策略就不进行描述了
          • 看那意思,这个扩容还会针对元素类型做出不同的处理方式
    • 之前了解过一个浅拷贝,就是用等于号,直接共享一个底层数组,现在有一个函数可以做到深层的拷贝,不再是共享底层数组了

      • copy函数直接两个参数就可以搞定,很容易理解,第一个参数是目标形成的新切片,第二个参数是源头切片
        • 这个顺序要理解,和我们操作的逻辑不太一样,是先把目标形成的东西放在了前面
    • 下面就是删除元素了

      • 我们知道切片的元素是连续的,从中间删除一个元素的话,被删除元素的后面元素全部都要向前移动

        • 我们知道切片的底层其实还是数组,所以这个删除是比较麻烦的,go并没有提供直接的删除方法(怎么说呢,我觉得go在这点上不够人性化,需要改进)

        • 这个时候删除我们可以借助append来实现,原理就是在 删除元素之前的元素组成的切片 的基础上去追加 删除元素之后的元素组成的切片

        • 可以看到有元素组成的切片的概念,这个概念就是用简单切片表达式去实现的

        • 实例:

          • 	// 从切片中删除元素
            	a := []int{30, 31, 32, 33, 34, 35, 36, 37}
            	// 要删除索引为2的元素
            	a = append(a[:2], a[3:]...)
            	fmt.Println(a) //[30 31 33 34 35 36 37]
            
          • 这个append函数是在原底层数组的基础上做的操作,所以相当于还是在原来的切片上做的操作

          • 这里的追加其实相当于覆盖,因为原来那个位置上面是有值的

          • append函数的使用方式不要忘记,第一个是原来的切片,第二个在这边因为用的也是切片,所以后面一定要加"…"来表示切片的全追加

      • 这里补充一下,用切片表达式去定义并初始化切片的时候,底层数组就是表达式的源头数组,所以源头数组发生改变时,切片也会发生改变(实验所得)

  • 字符串默认的类型是""

  • 突然发现,不用printf的%v,直接println也是可以输出结构体的吧,最起码这个切片类型是可以输出的

  • 这个sort内置排序函数的参数不可以是数组,切片可以,所以对数组进行排序时,先用切片包装一下

    • 这个sort排序函数的默认还需顺序是增序
    • 这个sort内置函数使用比较特殊,可以用点,点后面是待排序元素的类型
      • 实例:sort.Ints(slice1)

Map

  • map就是key-value的形式,内部使用散列表来实现的

    • 散列表就是指数据的物理位置存储是分散的,key和value之间有一个算法可以使得通过key可以在时间复杂度为1的情况下立刻找到对应的value,这个算法就是哈希算法
  • go中的map是引用类型,在使用之前必须先初始化

    • 突然想起来,切片也是引用类型,但是切片貌似不需要初始化就可以使用?或者是我理解错了,那切片的初始化是什么样子呢,貌似切片从定义开始就已经初始化了,哈哈哈
  • map类型怎么写(就是var一个map类型变量时怎么写这个类型)

    • 先是map表明自己是一个map-----切片的时候先是[]表明自己是一个切片
    • 然后[key类型]指明key的类型
    • 然后直接跟value类型表明value类型-----切片的时候是直接跟切片内元素类型
    • 总的来说就是map[keyType]valueType
  • 直接var一个map变量的话是nil类型,那肯定是没办法用的,所以必须初始化

    • 初始化也就是分配空间,分配空间的方式是什么呢
      • 用make函数,之前也用过make函数去定义并初始化一个切片,实际上也是在为切片分配空间,用了make,引用类型就不会再是nil了
        • 还记得切片如何用make定义并初始化吗?就是make([]切片元素类型,切片长度,切片容量)
          • 第一个参数并不是一个变量或者其他,而是告诉make函数自己到底要创建的是什么类型的变量,那么在make map的时候应该就是告诉make函数自己要创建一个map
        • 那么make map应该的写法是make(map[keyType]valueType,cap)
          • 这个cap是指map的容量,这个是可选的,但是看那个意思貌似是最好填
          • 疑问:这个cap如果我填好了,map可以像切片一样自动扩容吗
  • map的使用和map类型的定义差不多,map变量名[key]就可以返回该key对应的value了

  • 除了用make的方式初始化以外,和切片类型一样可以在声明的时候就初始化,而且还一样的是,用的都是花括号{}去括起来初始化数据那部分,里面写的形式是key:value然后很多对的话用逗号","分割一下

    • 值得注意的是用的是冒号来表示key和value之间的关系(咋说呢,搞得有点像json格式数据一样)

    • 实例:

      • userInfo := map[string]string{
        		"username": "沙河小王子",
        		"password": "123456",
        	}
        
  • make函数可以理解为根据参数分配空间,并且返回这个空间的首地址(感觉是这样)

  • 其实map的使用中,也就是 map变量名[key]相当于参数为key的一个函数,这个函数返回了两个值

    • 一个是map中传入key对应的value值

    • 一个是这个key是否在map中存在的判断结果

    • 我们平时如果不需要判断key是否存在时,可以直接用一个变量去接这个函数,需要的时候就用两个参数去接,这样两个都可以获得

    • 这样也是保证了一定的安全性

    • 实例:

      • v, ok := scoreMap["张三"]
        	if ok {
        		fmt.Println(v)
        	} else {
        		fmt.Println("查无此人")
        	}
        
      • 这个ok是一个布尔类型,在key不存在的时候这个v是valueType的零值,也就是说,如果value是int类型,那么v在这边就是0

    • 有这个东西的产生其实也是为了防止产生歧义,当返回结果是零值时你也不知道是人家map里面原本就没有key,还是说有key只不过这个这个key对应的value是零值,那这个情况肯定对程序影响很大的,所以这个是很重要的使用

  • for range遍历map

    • 可以把range map也看成一个随着for不断执行的函数,返回结果是key和其对应的value,只需要key时就只用一个变量去接,需要key和value时就用两个变量去接

    • 实例:

      • for k, v := range scoreMap {
        		fmt.Println(k, v)
        	}
        
      • for k := range scoreMap {
        		fmt.Println(k)
        	}
        
    • 这个遍历顺序和添加的顺序是没有关系的(其内部可能有自己的排序方式,毕竟在用户看来map是没有顺序的,当然也可能都没有,每次顺序都会变,哈哈哈)

  • 有了定义、初始化和遍历查看,下面看看如何删除一个键值对

    • 删除的话,想要直接删除看来只能是根据键来删,根据值的话可能还得自己写函数
    • 根据键来直接删除的话用的就是delete函数,第一个参数是map变量名,第二个参数是键就行
  • 突然想起来一个知识点,fmt包里面的Sprintf函数很特别的是,它有返回值,而且并不输出,返回的是一个格式化后的字符串,而Printf是会输出的,但是不清楚有没有返回一个格式化后的字符串,哈哈哈

  • 对切片使用append函数追加数据时需要注意,其中第一参数是需要追加的原来切片变量,append并不会改变这个切片变量,而是返回一个新的切片变量,所以用append方法之后一定要把结果赋值回去

  • 在数组或者切片的for range遍历方式中因为第一个循环变量index有的时候我们并不需要,但是是第一个循环变量省略了的话会影响结果,所以会用匿名变量来吸收,而在map的for range遍历方式中key我们往往是需要的,value作为第二个循环变量,可以不需要的时候省略是不会造成任何问题的

    • 那么也就可以发现这个上面对于range的理解没有问题,就是返回两个结果,index/key和value
    • 当函数返回两个结果,不想要第一个结果时必须用匿名变量吸收,不想要第二个结果时可以直接省略
    • 那么大胆猜测,我可以借用匿名变量实现在map的for range遍历中只输出所有value
  • 如何生成随机数呢

    • 随机数其实都是由一个数开始经过一些计算什么产生的,那个原始的数我们称之为种子
    • 因此想要生成随机数得先具有一个种子,如何生成种子呢
      • 可以借助时间生成一个种子,这其中会用到两个包,一个rand,一个time
      • 实例:rand.Seed(time.Now().UnixNano()) // 初始化随机数种子
    • 有了种子我们就可以用rand包里面的一个函数去生成数了
      • 实例:rand.Intn(100) // 生成0~99的随机整数(代码自行理解,也不难理解哈)
  • 想要按照我们想要的顺序去遍历map直接用map遍历显示是不可能的,因为map的顺序是随机的

    • 但是我们可以把key按照一定顺序存放在数组或者切片等有顺序的地方中,然后通过遍历这些地方的key去map中查找该key对应的value
    • 比如先老老实实把map中key按照map的遍历顺序存放在切片中,然后用sort包中的遍历函数对切片进行排序,然后借由遍历切片来按顺序遍历map的value
  • 添加键值对的方式比较简单,直接map变量名[key]=的方式去赋值就好

    • 这个之前其实可以去判断一下对应的key是否存在
  • 关于切片的浅拷贝有一个很好的理解

    • type Map map[string][]int
      m := make(Map)
      s := []int{1, 2}
      s = append(s, 3)
      fmt.Printf("%+v\n", s)
      m["q1mi"] = s
      s = append(s[:1], s[2:]...)
      fmt.Printf("%+v\n", s)
      fmt.Printf("%+v\n", m["q1mi"])
      
    • 这个结果是

      • [1 2 3]
        [1 3]
        [1 3 3]
        
    • 可以发现s改变了,m[“q1mi”]和s不一样的原因是,s和m[“q1mi”]之间是浅拷贝,也就是说共享了一个底层的数组,但是他们的长度和容量只是在刚开始的时候是一样的,其实还是各自管理各自的

      • s改变了底层数组,使得底层数组变成了1 3 3,然后s的长度变成了2,所以s最后结果是1 3,而m[“q1mi”]的长度是自己管理的,不受s长度的影响还是3,所以最后结果是1 3 3
    • 也就是说等于号去让两个切片变量相等时,他们只是共享了一个底层数组,但是他们的长度和容量是可以不一样的

    • 上面代码中还有一个知识点,就是如何给一个类型起别名

      • 首先是用type去表明我后面是要自定义了一个类型的
      • 然后是自定义类型的名字(相当于别名)
      • 然后是原来类型
      • 当有一个类型经常使用,但是名字太长时,可以用这种起别名的方式去简化和增强代码的可读性
      • 有一个包叫做strings,里面有很多关于字符串的方法,比如Split可以分割字符串成切片

函数

  • 在同一个包内,函数名不能重名,那也就是说没有java中的重载的概念

  • go中可以为函数的返回值指定变量名,当然也可以不指定直接写类型,这些指定的变量名是可以在函数体中直接使用的

  • 因为go中一个函数可以有多个返回值,那么这些返回值必须用一个括号包裹起来,看起来像一个整体

  • 当相邻的几个参数类型一样时,前面的参数类型可以省略,只保留最后一个参数的类型就行,这个在var定义的时候也是一样的操作

  • go中函数参数的个数可以是变化的,但其实是用一个切片来实现的

    • 首先可变参数通常是作为最后一个的,这样在函数体中获取时也比较清楚
    • 然后从使用函数的角度来看,我们确实是传入了可变的参数个数
    • 但是从定义函数和函数实现内部来看
      • 定义的时候其实还只是一个参数,只不过这个参数的类型是 …可变的参数类型
        • …来表示是一个可变参数
        • 后面再跟上这些可变参数的类型(那么岂不是说明可变参数必须是同一种类型?)
      • 函数实现内部来看,所有的可变参数都封装在了一个切片类型里面,函数定义时候的可变参数传递的变量相当于切片,通过遍历切片去获取可变的参数们
    • 其实吧,我们甚至可以直接用切片来实现可变参数,感觉这里面只是对切片的一个封装
  • 如果我们在函数声明的时候就指定了返回值的变量名,那么我们在函数体中给这个返回值变量进行正常流程的操作后,直接写一个return就可以返回这些返回值变量了,不需要在这个return后面追加上变量名,也是一种简写吧

  • 之前有用过type把某一类型设置成一个自定义的类型,现在还可以把符合某种结构的函数设置成一个自定义类型,我们称之为定义函数类型

    • 实例:type calculation func(int, int) int

    • 就是说符合上述结构的函数都可以是calculation类型,既然是一个类型,那么我们就可以定义一个该类型的变量,然后借用这个变量去调用对应的函数,就像正常调用函数一模一样

    • 需要注意的是定义了函数类型变量后是没有办法直接去调用函数的,因为符合一种函数结构的函数有很多,定义了之后并不知道自己具体是哪一个函数,所以必须先给这个变量指定函数,然后再正常使用

      • 实例:

        • func main() {
          	var c calculation               // 声明一个calculation类型的变量c
          	c = add                         // 把add赋值给c
          	fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
          	fmt.Println(c(1, 2))            // 像调用add一样调用c
          
          	f := add                        // 将函数add赋值给变量f1
          	fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
          	fmt.Println(f(10, 20))          // 像调用add一样调用f
          }
          
        • 和Python比较像的是,这个函数名可以像变量一样赋值,被赋值的变量可以像函数名一样去使用

  • 和Python一样

    • 函数可以作为参数传递
      • 参数传递方式就是“参数名 参数类型”
        • 参数名没什么可说的,自己想怎么命名就怎么命名,符合命名规范就行
        • 参数类型应该怎么写呢?
          • 这里的参数类型应该就是指符合某种格式规范的函数,比如参数类型,返回值类型这样
          • 这个时候就会联想到之前用type去定义的函数类型,这个地方可以直接写函数结构,也可以写定义好的函数类型(相当于函数结构的别名)
          • 这里补充一下函数结构的书写
            • 先是func表明是函数
            • 然后(参数类型,参数类型…)
            • 再然后空格后跟着返回值类型
            • 不难发现参数结构的书写是不需要跟着任何的变量名的,因为只是展示函数的结构,需要什么变量名呢
            • 比如:func(int, int) int
    • 函数也可以作为返回值类型
      • 比如根据参数的结果选择返回不同的函数
      • error错误也是一种类型
        • error类型的变量如何初始化一个数据呢
          • 我们可以借助errors包里面的New方法
          • 实例:errors.New("无法识别的操作符")
  • 在函数中是没有办法嵌套定义函数的,但是我们可以用匿名函数来解决

    • 匿名函数虽然没有函数名,但是把匿名函数赋值给一个变量就可以实现一样的函数调用效果

    • 当然不需要重复使用这个匿名函数的话可以在写完匿名函数后直接加括号写参数让它立即执行

    • 那么匿名函数的书写格式是什么样的呢

      • 先是func表明是函数

      • 然后没有函数名,直接括号放参数

      • 后面再跟着返回结果类型

      • 然后就是正常的花括号写函数体

      • 实例:`

        	func(x, y int) {
        		fmt.Println(x + y)
        	}
        
  • 闭包,一个封闭的包,包的含义其实就是指函数加引用环境(包中全部的东西),那么闭就是指引用环境只是给当前包中的函数使用的,包外是无法使用,这个包只提供了一个开口让外部使用,那么如何实现这个功能呢

    • 定义一个函数a,函数a内部定义了一个变量x,然后函数a返回的是另一个函数b,函数b中用到了这个函数a中的变量x,这个函数a就可以看成一个闭包,然后在外使用函数a时可以像使用b一样调用,相当于完全把a换成b来调用,然后每次调用a的时候其实都是共享一个x,x相当于java中静态的概念
      • 不管在闭包外定义多少个a函数类型什么的,用的都是一个x
    • 闭包可以看成是返回的函数加上一些引用的环境,就直接这样看会比较好理解,而且这个环境是静态的,累计变化的
    • 先是调用外层函数赋值给一个变量获取到闭包,然后对于这个变量像使用内层返回的函数一样去使用
    • 要想满足闭包的话,这个函数的返回值是不是也得是匿名函数
  • defer语句

    • 什么是defer语句,就是语句的前面加上“defer”修饰

    • “defer”修饰的作用是什么呢,就是这个语句不会立刻执行,而是延迟一段时间后再执行

      • 特别的,如果有很多语句都被defer修饰了,那么当defer的语句开始执行的时候,是最后被defer修饰的语句最先执行,以此类推(这是栈的方式,先进后出)
      • 那么defer语句要延迟到什么时候开始执行呢?
        • 首先语句肯定是写在函数里面的(main函数也是函数,函数外是不可以写除了定义全局变量等的语句的),所以defer语句肯定也是在函数里面的
        • 函数肯定是有返回的(返回值为空的函数运行结束的时候也是自动返回的),这个返回就是我们自己写的return语句
        • 这个defer语句就是延迟到return语句的时候执行的,那么到底是return语句的哪一步呢
          • return语句在底层不是原子操作,有两步,一个是给返回值赋值,一个是RET指令
          • 这个defer语句就是在这两步中间执行,也就是说在执行defer语句的时候函数的返回值已经确定了,那么这个时候往往需要做的就是释放资源等问题(比如:资源清理、文件关闭、解锁及记录时间等)
    • 大无语事件来了,下面这个代码的结果是5,6,5,5,我不是很能理解

      • func f1() int {
        	x := 5
        	defer func() {
        		x++
        	}()
        	return x
        }
        
        func f2() (x int) {
        	defer func() {
        		x++
        	}()
        	return 5
        }
        
        func f3() (y int) {
        	x := 5
        	defer func() {
        		x++
        	}()
        	return x
        }
        func f4() (x int) {
        	defer func(x int) {
        		x++
        	}(x)
        	return 5
        }
        func main() {
        	fmt.Println(f1())
        	fmt.Println(f2())
        	fmt.Println(f3())
        	fmt.Println(f4())
        }
        
    • 纠正一下,我前面说defer后面写表达式,其实不是,defer后面写的是函数回调,只能是函数回调

    • defer后面跟着的函数里面的某个参数可能又是调用的一个函数,这个延迟是无法延迟这个参数上的函数执行的

      • 我想我大概明白了这个defer使用的一些细节
      • 这个defer后面的函数确实是目前先不执行,但是它如果有参数的话,这些参数其实都已经计算好了,就等着return的时候开始执行
      • 所以如果defer后面紧跟的函数的参数有是一个函数的计算结果的话,这个结果会先被计算出来的
      • 突然发现,这个fmt.Println函数里面多个参数用逗号分隔的时候,输出会在参数间加空格
  • 之前我们知道用make来为引用类型的变量分配内存空间,其实还有一个内置函数叫做new,它是可以为值类型分配内存空间的,但是它返回的并不直接是这个值对象,而是指向这个内存空间首地址的指针

  • 除了make、new、len、append等内置函数外,还有一些其他的常用的内置对象

    • close:关闭channel通道
    • panic和recover:错误处理
      • panic和recover都是错误处理,有什么区别呢
        • panic可以在任何地方,而且是会让程序奔溃
        • recover只能在defer后面跟的函数的方法体中使用,是让程序恢复继续执行的
          • recover在defer后面的函数中,这个函数又是在return的时候执行,那么这个恢复会在正式返回前进行喽
          • 那如果程序中没有defer表达式,程序在出错奔溃时岂不是无法用recover去恢复
  • 错误日志怎么看

    • 其实控制台打出来的日志中,最上面的才是程序出错的最根本的地方,下面都是由于这个原因而引发的出错地方,比如使用了这个出错的地方的东西
  • panic怎么使用呢

    • 其实很简单,直接panic(“报错提示语句”)就可以了
  • recover如何配合着使用呢

    • 因为recover是只能在defer后面的函数的函数体里面写的,所以我们需要在容易出错的地方加上defer表达式,一般是跟着匿名函数的
    • 在匿名函数里面直接recover()就可以恢复了,这个函数是有返回值的(具体是什么内容我就不知道了)
      • 我们可以通过判断这个返回值是否为nil来判断当前程序有没有发生panic,没有的话其实这个函数也不会有什么影响
    • 值得注意的是,这个defer表达式一定要在可能会发生panic的语句前面写,因为一旦panic在前了,后面的defer也是不可能执行了
  • 遇到大写和小写处理方式一样的时候,直接全转为大写或者小写

  • strings包中有一个方法可以统计字符串中某个字符串出现的次数,是Count方法


指针

  • 地址就是指针,保存地址的变量就是指针变量

  • 指针类型的格式化是%p,注意这里的P是小写的

    • 就是说类似于“*int”定义出来的变量或者是“&变量名”得到的变量就是指针变量,然后这个变量表示的是地址,用%p去格式化输出的话是一个16进制的地址数,%d的话是10进制的地址数
    • 可以理解为这边的%p是对值的16进制输出
  • 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间

    • 所以下面代码会引发panic

      • 	var b map[string]int
        	b["沙河娜扎"] = 100
        	fmt.Println(b)
        
      • 这个map仅仅只是声明了,需要通过make函数来分配内存空间

  • 因为值类型的内存空间大小是确定的,所以new函数去分配内存空间的时候,只需要一个参数,就是到底是哪个值类型

    • new函数返回的是指向分配好的地址的指针
    • 并且这个分配出来的内存空间中的值是该类型的零值
    • 布尔类型的默认值是false
    • 这个new函数什么时候需要使用呢
      • 一般是用var的方式去声明了一个指针类型,然后这个指针类型其实是一个引用类型,必须要先分配空间才可以使用
      • 然后和其他引用类型不同的是,指针类型并不是用make去分配内存空间,而是用new
      • 我们知道这个new函数的返回值恰好就是一个指针,所以直接把new函数的回调赋给指针类型变量就算是完成了指针类型的初始化
  • make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身


结构体

  • 之前type的写法其实是自定义一个类型,只不过有的时候我们是基于已经存在的类型去自定义类型,造成了一种别名的假象

    • 当然type也有起别名的方式,就是type 别名=原名
    • 也就是说加了等于号就是起别名,没有等于号就是自定义类型
    • 复习之前学过的两个类型byte和rune,其实内部就是用别名的形式做的
      • byte是uint8,只能表示ascll码上的字符
      • rune是int32,可以表示更多的字符,包括汉字和日文等,依据utf-8
  • 我们自己定义的类型,格式化输出后会加一个“包名.”来表示是哪个包下面的

  • 起别名的话本质还是原来的类型,自定义类型的话就是一个新的类型了,即使两者之间可能没什么区别,但是应该也不能用等号去赋值,因为已经不是同一个类型了吧

    • 而且,自定义的类型在编译完成后就不会存在了(其实也不是很明白这个区别怎么了)
  • go语言中通过结构体来实现面向对象

  • 结构体如何定义

    • 因为结构体属于一个新的类型,所以我们需要用到type自定义类型的写法
    • 然后类型名后面跟的是一个结构体的结构“struct{}”
    • 在花括号里面写上结构体所具有的字段(没错,在java中叫做属性或成员变量,在go中叫做字段)
      • 字段的格式是“字段名 字段类型”
      • 几个字段之间直接换行,不用加逗号什么的
      • 然后同一种类型的字段可以写在一行,像这样name, city string
  • 来了解一下对于结构体来说几个格式化输出的区别

    • %v:只输出值,并不是道值属于结构体中的哪个字段
    • %+v:输出字段名和字段值,并不知道结构体的类型名
    • %#v:在上一条的基础上加上结构体的类型名(记住一般是包名.结构体类型名)
    • 由此可以看出%#v更加详细
  • 和C语言一样,通过点的方式获取字段

  • 只是需要一个临时的结构的时候可以定义匿名结构体,其实就是var 变量名 struct{…}

    • 不给struct命个名而已
  • 很容易忘记的一个点是,new出来的变量并不是new函数参数类型变量,而是一个指向这个类型变量地址的指针,记住是指针

    • 不过幸运的是,对于结构体而言,结构体指针和结构体变量一样,可以直接通过点的方式去获取字段等操作
    • 哦对,是可以用new的方式去搞出一个结构体指针的
    • 还有,针对于结构体指针也可以直接用%#v的形式去打印出该结构体的内容,不同的是,因为这是一个指针,所以在打印结构体类型的时候,会加一个&表明是一个指针,然后再是包名.自定义结构体名称
    • 所以这个结构体指针的使用还是挺方便的,比C语言方便,但是自己还是需要注意一下,现在是结构体变量还是结构体指针变量
  • 除了new函数分配地址并获取结构体指针外,还可以直接用“&结构体类型名{字段初值}”的方式去分配地址并同样获取到结构体指针,等同于new函数

  • 结构体的使用

    • 可以在声明的时候就用花括号,然后键值对的方式初始化
      • 和之前一样键值对方式需要加逗号
      • 非常神奇的是,即使声明的时候用了&,表明是一个结构体指针,也可以像结构体变量一样正常加花括号去初始化
    • 也可以声明好后用点的方式赋初值,看自己选择
    • 绝了,还有一种简化的初始化方式,就是花括号里面直接写值,不写字段名,但是存在一些局限性
      • 首先这个顺序和定义结构体的顺序必须要一样
      • 然后这个字段是一个都不能少啊,不然对不少,并不支持说可以省略后面的不写这种事情
      • 最后就是如果用了这种方式,花括号里面就只能用这种方式,不可以和键值对的形式混合着用
  • 空的结构体是不占用内存空间的,这个空的结构体是什么意思的,就是说这个结构体没有字段,而不是说字段值不赋值哦

    • 这里补充unsafe这个包下面有一个数Sizeof,可以得到一个变量所占用的内存空间大小(单位应该是字节)
  • 我的天,一个面试题把我给炸出来了

    • 首先,我们得先了解for range的一个原理

      • 在for range里面有一个循环变量,这个range每次都会返回一个元素,然后这个元素的值就会赋值给循环变量
      • 这个循环变量是单独存在的,有自己的内存空间的,不是指切片或是其他里面真的元素,相当于那个元素的副本
    • 实例:

      • type student struct {
        	name string
        	age  int
        }
        
        func main() {
        	m := make(map[string]*student)
        	stus := []student{
        		{name: "小王子", age: 18},
        		{name: "娜扎", age: 23},
        		{name: "大王八", age: 9000},
        	}
        
        	for _, stu := range stus {
        		m[stu.name] = &stu
        	}
        	for k, v := range m {
        		fmt.Println(k, "=>", v.name)
        	}
        
      • 上面这个代码的结果会发现key的值是对的,但是value的结果却始终都是切片中的最后一个student

      • 因为value的值是那个循环变量的地址,这个循环变量的值在循环过程中是一直在变化的,最后这个值是停留在了切片中的最后一个student类型

      • map的value中都是这个循环变量的地址,那么由这个地址指向的name自然都是一样的,且是最后一个student实例的名字

      • 这个时候如果我把map的类型换掉,把map里面元素的类型由结构体指针直接换成结构体

        • 在for range切片的时候,自然value也要换成stu而不是&stu
        • 那么这个时候结果就没有问题了
        • 因为value上的值是循环变量指向的值,这个值该是什么就是什么
      • 感觉这个结构体可以算是一个值类型吧,更偏向值类型一点,毕竟它是用new的方式去分配地址的嘛

        • 解密了家人们,我看到了,结构体确实是值类型
  • 感觉加个&就可以实现值类型到引用类型的转变(实际上类型肯定是没变的哈)

  • 我们可以写一个函数,接收一些参数,然后返回一个结构体指针

    • 这样的函数就神似java中类的构造方法
    • 需要注意的是构造函数返回的是指向那个结构体的指针哈,为什么用的是指针而不是结构体本身呢
      • 因为当结构体很复杂的,我们将构造函数返回给一个结构体变量的时候,是一个一个的值赋值,这个开销是很大的(虽然这个在我们外人眼里看不到,但是实际上程序是做了大量的赋值的),而如果只是一个地址的话就会方便很多
  • 之前学的函数其实都是简单的可以直接调用的函数,就像是java中的静态函数一样

    • 其实还有一种特殊的函数,我们称之为方法
    • 方法比普通函数不同的是,它不是直接使用的,而是需要一个特定类型的变量以点的形式调用
      • 就好像是java中的非静态方法一样,它需要用实例化后的对象去以点的形式调用
    • 方法的书写和普通函数不同的是在函数名之前多了一个括号,把接收者类型确定下来,确定下来后这个类型的变量就可以用点的形式去使用这个方法了
    • 这个接受者的命名没有强制要求,但是建议就用一个字母表示,这个字母是接收者类型首字母的小写
      • 其实这也是在规定,让我们把自定义的结构体的名字定义为首字母大写的
    • 这个接收者在函数体中也是可以正常使用的
      • 那么这个时候就会出现一个问题,函数能不能改变这个接受者,这个其实是分情况的
        • 当规定这个接受者是一个引用类型时,那么函数是可以改变接受者的
        • 当规定这个接受者是一个值类型时,那么函数是不可以改变接受者的
        • 这个引用类型包含两类
          • 本身就是引用类型,比如切片、map等
          • 是指针类型的变量
        • 值得注意的是,结构体不是引用类型,是值类型,所以如果规定的接收者类型不是指针的话,在方法内部对接收者的改变是不会应用于外部的
        • 还有一点,接收者类型和结构体相关时,无论方法规定的接收者类型是引用类型还是值类型,在调用的时候都可以用结构体指针点的方式去调用,既可以看成是指针去点,也可以看成是一个go为我们提供的语法糖,指针点变成指针所指向的对象点
    • 感觉这个go吧,就是把对象的概念给拆开来了
      • 对象的成员变量用结构体去设置
      • 对象的构造方法就写一个返回类型指针的函数
      • 对象的成员方法就为这个对象写接收者方法
      • 而且这个结构体的指针可以直接用点的方式去做和结构体变量一样的操作,感觉这个指针做出了引用类型那味
    • 值类型是不需要初始化的,因为初始化的目的就是为了具有内存空间,值类型一定义内存空间就确定下来了
    • 还有一点很重要,就是方法接收者只能是本地类型,也就是本包的,那么光int类型肯定是不能作为接收者的,起别名的方式也是不管用的,怎么办呢
      • 我们可以用type以int作为基础去自定义一个新的类型,当然我们知道这个类型和int是一样的作用
      • 然后把这个自定义的类型作为接收者
      • 那么其实也就说明了int这种类型以及其他go官方定义好的类型都不属于本地类型
  • 匿名字段,顾名思义,就是结构体的字段是没有名字的,但是名字可以没有,字段类型肯定要有的吧

    • 那么没有名字要如何使用这个字段呢
      • 其实吧,在go内部实现的时候,是把这个字段的类型名作为了字段的名字
      • 因此在使用的时候就“结构体点字段名”就可以使用了
      • 因为结构体中字段名不可以重复,这就要求这个匿名字段的字段类型在结构体中是不能重复的
  • 结构体中有字段也是一个结构体,那么在用花括号这种方式初始化的时候这个字段应该怎么写呢

    • 首先一般还是键值对的形式(不是键值对的话自行想象吧,反正也差不多),在冒号后面是这样写的“结构体类型名{键值对形式的字段}”
    • 可以发现这个写法其实和外面的结构体写法是统一得
    • 我发现了好多都是变量类型然后花括号这种方式,好像只要这个类型的值不止一个时,就用这种方式,很统一,哈哈哈
  • 妈呀,这个匿名字段居然是可以和正常的字段定义混用的,当某个类型在这个结构体中只会出现一次的时候,这个字段就可以用匿名字段

    • 一般这个匿名字段用结构体感觉会多一点

    • 我的天哪,这也太神奇了吧,天知道怎么回事儿,这个匿名字段类型如果是结构体的话

      • 那么我们在使用这个结构体的字段时,居然可以省略这个匿名字段,啥意思呢,见下

        • 	user2.Address.Province = "山东"    // 匿名字段默认使用类型名作为字段名
          	user2.City = "威海"  
          
        • 按道理说应该和Province的使用一样中间再加一个Address的,但是Address是一个匿名字段,因此这个Address是可以省略的

        • 疑问:那么问题来了,如果两个匿名字段都是不同的结构体,但是这两个结构体中有一样命名的字段怎么办,外面调用的时候怎么知道是谁的字段呢

          • 破解了,姐妹们,这个冲突的时候匿名字段类型作为字段名是不可以省略的哈,指定就行
        • 这个寻找字段的逻辑是,现在本结构体中找字段,找不到的时候去匿名字段的结构体中找

  • 感觉一般这个方法的接收者用引用类型比较多,也就是一个指针

  • 结构体匿名字段在使用时可以省略的特性可以做成是继承的感觉

    • 是这样,如果我们想让一个类型a去继承另一个类型b,也就是a类型可以使用b类型中所有的字段和方法,那么我们可以让b的指针类型作为a的匿名字段
    • 这样的话,a的变量或者指针变量就可以省略匿名字段去调用这个匿名字段的字段和方法,这从外界看来就好像a除了有了b之外,自己还有一些扩展的内容
    • 没错,相信可以看出来,这个a是既可以省略匿名变量用其字段,也可以用其方法
    • 突然发现,这个可以实现多继承
      • 只要包含多个匿名变量指针就可以了
    • 这边有个注意点就是,这边实现继承的时候关于匿名变量是一个结构体指针,而不是结构体变量,那么这个有什么区别呢?还是说只是为了减少开销?
  • 其实吧,感觉只有用了指针才是真正的看到了对象的概念,所以想要有对象概念的时候,结构体变量都换成结构体指针

  • go中也实现了一部分java中的变量私有不私有的问题

    • 不过这个私有仅仅只是是否在这个包中私有不私有(突然想到同样的我们可以写get和set方法去把私有的变量变成不私有的)
    • 疑问:那么java中方法私有不私有的实现go中有吗
    • 那么如何实现这个私有不私有呢
      • 其实很简单,直接看字段命名,大写就是公开,小写就是不公开
      • 那么匿名变量就看匿名变量类型名的首字母是大小写还是小写喽
    • 这个会不会有很多引申啊,比如方法也是这样什么的,暂时不清楚
  • json格式的键一定是字符串类型,所以一定会用一个引号包裹起来,感觉有点像是map对键有了特殊的要求

  • 突然发现Sprintf这个还蛮好用的,比如

    • 	stu := &Student{
      			Name:   fmt.Sprintf("stu%02d", i),
      			Gender: "男",
      			ID:     i,
      		}
      
    • 这里name有了一个要求,就是数字是2位的,不满足2位时会用0去填充,这个格式逻辑代码实现是很麻烦的,但是这个函数就方便快捷地完成了

  • json序列化和json反序列化怎么做呢

    • 首先这个序列化的对象肯定是结构体,这个要清楚
    • 然后有一个包,包里面两个方法
      • Marshal方法:一个参数,就是需要序列化的结构体,返回两个结果
        • 一个是序列化结果
        • 一个是是否报错的信息,如果这个结果不是nil的话,就说明序列化出错了,那么第一个返回结果也就不用看了
      • Unmarshal方法:两个参数
        • 一个是需要反序列化的切片
          • 这个是什么意思呢,首先我们拿到的应该是一个字符串,我们需要用byte()方法吧这个字符串强转成数据是byte的切片类型,得到结果是一个切片,切片的每一个元素就是字符串中每一个字符其实
          • 疑问:是byte类型的切片的话,可以不可以用rune呢
            • 实验证明:并不可以
          • 这边用byte关于汉字也是可以正常转的哦,神奇
        • 一个是反序列化结果赋予的值(这个值貌似是一个结构体指针)
          • 序列化方法中没有是因为序列化操作不需要管原来结构体类型,而反序列方法需要知道json字符串要反序列化成什么样子
        • 这个方法只有一个返回值,就是是否错误的信息,如果不是空,那说明反序列化出现问题了
    • 非常非常智能的是,在反序列化过程中,这个字符串只管写内容就行,什么意思呢
      • 需要转换成的结构体中有一个字段是指针类型(引用类型),没关系,json字符串中不需要写指针(也就是地址),直接写指针所指向空间的值就可以,在这个反序列化过程中,go会自动帮我们搞的(分配空间、赋值然后返回指针,感觉是这样,哈哈哈)
  • 结构体标签tag

    • 首先,tag是谁的tag
      • tag是打在了结构体的字段的后面,每个字段的后面都可以打tag
      • 而且这个tag并不是唯一的,可以有多个tag
    • 这个tag有什么用呢
      • 在反射的时候被读出来
      • 比如在序列化的时候,我们并不想用默认字段名作为json格式中的key,我们想要指定某个字段在序列化时的key怎么办呢
        • 我们可以这个字段打上json的标签,就是这样
        • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ixbrOEE6-1633744694376)(C:\Users\qyy\AppData\Roaming\Typora\typora-user-images\image-20211008160749928.png)]
        • 由此我们也能看出其实这个序列化的过程其实用到了反射机制,因为标签是反射的时候被读出来的
        • 而且,我看这个例子中,标签是键值对的形式,感觉这个键是固定的,并不是我们可以随意捏造的
      • tag其实是有一定格式要求的,而且要求严格,有略微偏差都会出问题,那么具体格式是什么样子的呢
        • 首先tag的内容必须用` `包裹
        • 然后里面是键值对的形式
          • 其中键是没有引号的,直接写
          • 值是一定要用双引号包裹起来的
        • 几个tag之间是要用空格间隔的
        • 千万不要加什么多余的空格
        • 很重要的一点是这个tag的格式如果写错了,程序在编译和运行的时候是不会报错的,就只是呈现出一个错误的结果,所以一定要注意啊
  • 这里关于序列化和反序列化有一个问题

    • 这个序列化和反序列化用的都是一个外面包,叫json
    • 我们知道结构体中如果字段的命名首字母没有大小的话,其他包是无法访问的,那么也就是导致这个字段并不会被序列化和反序列化
    • 所以这就要求我们注意,如果我们有一个结构体需要序列化和反序列化的话,其字段最好命名成首字母大写
      • 疑问:这边是否存在一个get和set的方法去辅助这个私有字段的序列化和反序列化的反射呢
  • 回顾一下,有一个copy的内置函数实现的是深拷贝,就是不断的赋值

    • 浅拷贝自然就是等号喽
    • 这里遇到一个问题,就是如果想用copy实现深拷贝,那么目标变量的内存空间必须分配好,因为copy函数并没有为变量分配内存空间这个操作,它只是把值一个一个地放到目标变量准备好的内存空间中去
      • 比如,我们想拷贝切片b到切片a上去,那么切片a必须先分配好内存空间,比如用make内置函数去分配,否则这个拷贝是不成功的
      • 也就是说引用类型必须先初始化,才可以用深拷贝copy这个函数
  • 这里在赋值的时候一定要注意你希望的是引用类型还是值类型如果本身是引用类型,但是你希望是赋值副本的话,就需要先为目标引用类型make出内存空间,然后使用copy内置函数或其他方式去进行深拷贝

  • 实验发现,切片表达式中的数如果越界是不会报错什么的

  • 这个string在go中是值类型,所以相等的判断和java中不太一样,是直接用等号就可以解决的

  • 用for range时多了一个感悟

    • range的结果直接数据不接下标的话,真的就只能用来遍历,想要改变值一定需要一个下标

    • 因为这个接的变量是单独存在的,我们只用这个变量去改变的话,原来数组或者切片等等的值并不会发生根本改变

    • 除非你自己再想想什么地址和值的转变啥的

      • 实例:(这个学生列表里面存放的是学生的地址)

        • // 编辑学生信息(希望是值传递)
          func (s *StudentManage) EditStudent(student Student) int {
             for index, studentItem := range s.StudentList {
                if studentItem.Id == student.Id {
                   // 法一:更换地址指向
                   s.StudentList[index] = &student
          
                   // 法二:改变那个地址上的值
                   //studentItem.Id=student.Id
                   //studentItem.Name=student.Name
                   //studentItem.Score=student.Score
                   //studentItem.Age=student.Age
          
                   return 1
                }
             }
          
             return 0
          }
          
  • 只有切片才可以用copy函数,结构体是不可以的

  • 这个引用类型和值类型是真的要注意,根据需求来判断,但是感觉有点乱


  • 包可以实现代码复用

  • 什么是内置包

    • 就是内置在go的sdk里面的,不需要我们自行去下载或编写的,下载sdk的时候就已经包含了这些包
    • 比如我们常用的fmt包,还有之前用到过的unsafe包(里面有个函数可以获取变量所占的字节数)、json包(里面有序列化和反序列化的函数)、strings包(里面有关于字符串操作的很多常规函数)、sort包(里面有关于排序的常规函数)等等
  • 自己编写包有什么需要注意的地方

    • 首先一个包其实就相当于一个文件夹,包下面全是.go文件,就像java的包下面全是.java文件一样
      • 和java不同的是,java的文件夹名就是包名,但是go的文件夹名和包名可以不一样,但是最起码一个文件夹下的文件import的包都得是是一个包名
    • 包名不能包含“-”符号,这是为啥呀
  • 只有main包在编译完成后才会生成可执行文件,其他包下面的不会

  • 把包中的常量、变量、类型、函数等名字的首字母大写,这外面的包就可以访问了(貌似所有的相关命名的规则都是这样的)

  • import包时的注意点

    • 在package声明包的时候是不需要给包名加引号的,但是在导入包的时候需要加引号
    • import后面跟的是包的路径名,也就是文件夹的名字(主要是留意文件夹名和包名不一样的情况)
      • 虽然import的时候写的文件夹名,但是在使用包中内容的时候还得是“包名.”的形式
    • 这个路径默认情况下是从$GOPATH/src/(是此电脑里面一步步打开的环境变量,不是idea里面的)开始的,也就是在go的sdk里面的,不是我们自己写的
      • 内置函数的包就在这个src里面吧
    • 像我们在src下面的main需要引用另一个我们自己写的包中的内容时,我们import后面的路径还得写全呀
    • 写全的路径是从哪开始的呢,就从项目名开始就可以了
    • go语言的包禁止循环导入
  • 将很多包写在一个import里面的时候,那些包要用括号包裹,然后包与包之间直接换行就好

  • 不同的文件目录下的包名可能写重复了,或者包名太长了,怎么办呢

    • 我们可以给包起别名,就在包的路径前面加上别名就好
    • 自己起的名字是不需要加上引号的,但是引用别人的名字就需要加上个引号
  • 非main包下面的文件虽然不会编译成可执行文件,但是当他们被main使用时,会被编译进main形成的可执行文件中

  • 如果给包起的别名是“-”的话,那么这个包就是个匿名包,无法使用包内部数据

    • 疑问:匿名包又为什么要导入呢
    • 虽然无法使用,但是还是会被编译到可执行文件中
  • 在.go程序中可以写一个init初始化函数,这个函数会在这个程序所属于的包被import的时候自动执行(只能是以这种方式执行,不能自己去调用)

    • 这个调用还是提前于main的

    • 如果这个程序所属于的包是main包的话,那么这个程序运行的时候,就会执行里面的init函数

    • init函数写法:

      • 没有参数和返回值就行
    • 如果有嵌套的import包,又都有init函数,那么init函数的执行顺序是什么样的

      • 从main的角度来看,main调用了包a,包里面又调用了包b,包b又调用了包c,那么是最根本的包c里面的init函数最先执行
  • 关于panic和recover

    • 如果对panic进行了recover操作,那么程序执行的结果是不会显示出panic函数调用时的错的,会让外人感觉程序好像没有出错的样子,但其实结果是不对的,所以感觉正常的错还是应该让他报出来

接口

  • 看到接口,我们只能知道这个接口能做什么

  • go提倡面向接口编程,这样可以简化代码,谁不爱呢

  • 接口是一个类型,既然是类型就需要用type关键字去定义,定义方式和定义结构体很像

    • 把结构体的struct换成了interface表示接口
    • 接口里面是很多方法,方法定义的书写格式是“方法名(方法参数列表) 返回值列表”
      • 疑问:这个返回值列表里面值是多个的话要加括号吗
      • 这里面不是申明变量,所以不用加func,只需要有个必要的格式就行,go自己也知道接口里面写的肯定是方法
  • 接口命名的后缀最好是er,感觉拟人化了

  • 同样的接口命名首字母是否大写,和接口中方法名首字母是否大写决定了这个接口是私有还是公有

  • 因为接口只是定义了个形式,所以接口中的方法中的参数和返回值的变量名可以直接省略

    • 疑问:有多个相同类型的变量或返回值也可以省略吗
  • 接口所有方法如果被一个对象全部实现了,那么就说这个对象实现了是个接口,但是其实一个对象并不一定需要完全实现这个接口的所有方法

    • 可以是这样,一个对象实现了一个接口中的部分方法,然后这个对象中有一个字段类型,它又实现了这个接口中的剩余方法(当然还有其他情况,自行想象)
  • 接口如何实现

    • 其实就是接口中的每一个方法都在外写一个带有接收者的同款方法,这个接收者就是这个接口的实现者
  • 可以把接口的实现对象的实例直接赋值给接口类型对象,然后正常使用,和java一样

    • 这里有个注意点,如果接收者是值类型(也就是不取地址),那么赋值给接口类型对象的可以是值对象本身实例也可以是这个实例的指针

      • 因为这里有个语法糖,go会为这个指针取值
    • 如果接收者是指针类型,那么赋值的东西只能是指针类型,不能是值类型本身的实例

    • var peo People = Student{}
      
      • 看到上面的代码就需要注意了,一个Student类型的结构体却赋值给了一个People类型的变量,就应该想到可能是Student对象实现了People对象,那么就需要注意这个Student对象在实现的时候接收者是它的值类型还是指针类型,如果是指针类型的话,那么这个等式就是不成立的
    • 疑问:如果不谈这个接口的问题,就是一个对象的指针作为接收者写一个方法,那么我们可以直接用这个对象的值类型去调用吗

      • 实验表明是可以,也就是说这个不管用值类型还是引用类型直接去调用时没问题的,但是如果要赋值给接口类型,再让接口类型去调用的话,就需要注意赋值的时候到底应该是什么,那干脆赋值时全赋指针类型好了
  • 下面这个代码不能理解

    • Tips: 观察下面的代码,体味此处_的妙用

    • // 摘自gin框架routergroup.go
      type IRouter interface{ ... }
      
      type RouterGroup struct { ... }
      
      var _ IRouter = &RouterGroup{}  // 确保RouterGroup实现了接口IRouter
      
  • 一个类型可以实现多个接口

    • 疑问: 那万一这多个接口的方法名中有重复怎么办
  • 接口嵌套

    • 就是接口里面不仅仅有规定好形式的方法,还有待实现的接口
    • 这个待实现的接口就直接写接口类型就行了,其他也不需要
    • 然后实现这个嵌套接口的时候,就当这个嵌套不存在就行,正常使用
  • 空接口

    • 就是没有规定需要实现的方法,理解为不需要实现方法的接口

    • 因此,任何类型的实例都可以赋值给这个接口类型变量

    • 那这个空接口有什么用呢

      • interfacr{}直接这样的话感觉有点像是匿名接口,但是这个可以直接表示一种变量的类型是接口

      • 因为任何类型都可以赋值给空接口类型变量,那么如果我们用这个类型作为函数的参数,那么这个函数岂不是变成了可以接收任何类型的参数

      • 实例:

        • // 空接口作为函数参数
          func show(a interface{}) {
          	fmt.Printf("type:%T value:%v\n", a, a)
          }
          
      • 还有啊,之前我们说map和切片都是定义死了这个value的类型的,那么如果把这个类型定义成空接口类型,这个map或者切片岂不是就可以存储任意类型的东西了

      • 感觉这个空接口就像是java中的Object类型一样

  • 接口既然也是一种类型,那么接口在赋予了实例的时候就也是有值的

    • 接口的值其实分为两部分,一个是实例具体类型,一个是实例这种具体类型的值
    • 我们称这个类型叫动态类型,这个值叫动态值
  • io这个包中有一个空接口,是Writer

  • 断言,判断是哪种语言,在程序中就是判断变量是哪种类型的

    • 因为接口类型可以被赋予多种类型的变量,如果想知道这个接口类型到底是什么值,我们就需要用到断言

    • 用法很简单,就是变量名.(我们猜测的变量类型)

      • 实例:

        • func main() {
          	var x interface{}
          	x = "Hello 沙河"
          	v, ok := x.(string)
          	if ok {
          		fmt.Println(v)
          	} else {
          		fmt.Println("类型断言失败")
          	}
          }
          
    • 这个方法很特别的是,你用了这个方法,如果判定确实是这个类型的话,它会帮你转化为这个类型的变量作为一个返回结果,第二个结果就是判断是否是这个类型的结果

      • 下面这种判断还挺巧妙的

        • func justifyType(x interface{}) {
          	switch v := x.(type) {
          	case string:
          		fmt.Printf("x is a string,value is %v\n", v)
          	case int:
          		fmt.Printf("x is a int is %v\n", v)
          	case bool:
          		fmt.Printf("x is a bool is %v\n", v)
          	default:
          		fmt.Println("unsupport type!")
          	}
          }
          
        • 直接只要第一个返回结果,然后对第一个返回结果进行判断,如果都不是我们猜想的类型的话就用一个default显示一下

    • 空接口在go中的使用十分广泛

  • 根据文件名判断这个文件是否存在的方法

    • func CheckFileIsExist(filename string) bool {
         if _, err := os.Stat(filename); os.IsNotExist(err) {
            return false
         }
         return true
      }
      
  • 创建文件、打开文件、写入数据和关闭文件的操作

    • // 文件实现这个打印日志接口
      func (f *File) PrintLog(logString string) {
         // 定义文件类型变量
         var file *os.File
      
         // 先判断这个文件是否存在
         if CheckFileIsExist(f.LogFileName) {
            // 文件存在时,打开文件
            file, _ = os.OpenFile(f.LogFileName, os.O_APPEND|os.O_WRONLY, 0666)
         } else {
            // 文件不存在是,创建文件
            file, _ = os.Create(f.LogFileName)
         }
      
         // 最后关闭文件(文件有打开就有关闭)
         defer file.Close()
      
         // 开始写入数据
         n, err := io.WriteString(file, logString+"\n")
         if err != nil {
            panic(err)
         }
         fmt.Printf("写入 %d 个字节\n", n)
      }
      

反射

  • reflect包里面有两个全局变量,Type和Value,表示变量的类型和值

    • 然后通过TypeOf和ValueOf函数可以分别获取到变量的类型和值

    • 这两个函数返回的结果就是对应的全局变量即reflect包里面的Type和Value

    • TypeOf函数返回的结果格式化输出(%v)后就是类型的名字,不是这种类型的对象,而是类型本身,比如

      • package main
        
        import (
        	"fmt"
        	"reflect"
        )
        
        func reflectType(x interface{}) {
        	v := reflect.TypeOf(x)
        	fmt.Printf("type:%v\n", v)
        }
        func main() {
        	var a float32 = 3.14
        	reflectType(a) // type:float32
        	var b int64 = 100
        	reflectType(b) // type:int64
        }
        
      • 会发现这个v就是“float32”和“int64”

  • reflect包中的变量Type有两个方法可以调用

    • Name方法:
      • 如果这个Type是底层类型,那么返回是空
      • 如果是自定义类型,那么返回自定类型的名字
    • Kind方法:
      • 如果这个Type是底层类型,那么返回是底层类型名
      • 如果是自定义类型,那么返回还是底层类型名
    • 这里的底层类型名并不是我们用type关键字去定义的时候依据的那个类型的写法,而是说这个类型属于什么类型
      • 比如我们写type peoples []people,那么这个类型的Kind不是[]people,而是Slice
    • 方法的返回结果可以用%v去打印
  • 这个Kind的返回结果有哪些可能呢?

    • type Kind uint
      const (
          Invalid Kind = iota  // 非法类型
          Bool                 // 布尔型
          Int                  // 有符号整型
          Int8                 // 有符号8位整型
          Int16                // 有符号16位整型
          Int32                // 有符号32位整型
          Int64                // 有符号64位整型
          Uint                 // 无符号整型
          Uint8                // 无符号8位整型
          Uint16               // 无符号16位整型
          Uint32               // 无符号32位整型
          Uint64               // 无符号64位整型
          Uintptr              // 指针
          Float32              // 单精度浮点数
          Float64              // 双精度浮点数
          Complex64            // 64位复数类型
          Complex128           // 128位复数类型
          Array                // 数组
          Chan                 // 通道
          Func                 // 函数
          Interface            // 接口
          Map                  // 映射
          Ptr                  // 指针
          Slice                // 切片
          String               // 字符串
          Struct               // 结构体
          UnsafePointer        // 底层指针
      )
      
  • 通过reflect包中的ValueOf函数,我们可以得到reflect包中的Value类型变量

    • Value类型变量有一个Kind的方法可以调用,获得的结果是reflect包中自定义的底层类型(形式就是reflect.底层类型)

    • Value类型变量还有一类方法可以调用,获得的结果是对应类型的值

      • 这类方法的名字和返回结果类型见下

        • Interface() interface {}	
          将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
          
          ② Int() int64	
          将值以 int 类型返回,所有有符号整型均可以此方式返回
          
          ③ Uint() uint64	
          将值以 uint 类型返回,所有无符号整型均可以此方式返回
          
          ④ Float() float64	
          将值以双精度(float64)类型返回,所有浮点数(float32float64)均可以此方式返回
          
          ⑤ Bool() bool	
          将值以 bool 类型返回
          
          ⑥ Bytes() []byte	
          将值以字节数组 []byte 类型返回
          
          ⑦ String() string	
          将值以字符串类型返回
          
  • reflect包里面的Value类型变量想要获取到其指针类型的话,要用这个变量的Elem()方法,这个方法的返回值就是一个&Value

    • 我们直接拥Value类型对象调用Set方法时,是会panic的,因为这是一个值传递,所以我们需要用的是Value类型对象先调用Elem方法获取到指针,然后指针去调用Set方法修改变量值
    • 修改变量值的Set方法全称是Set+底层类型,比如Value类型的Kind是int的时候,我们就用SetInt方法去修改Value类型的值
  • 函数在定义的时候参数写的是值类型(没有指针),但是我们在调用的时候参数传递指针类型是完全可以的,并且这个是个引用类型,可以修改原值

  • Value类型还有一个IsNil方法去判断这个类型的值是不是nil,但是有要求,这个Value类型必须本身的零值就是nil(通道、函数、接口、映射、指针、切片),不然就是panic

  • Value类型还有一个IsValid方法判断这个类型的值是否有效

    • 是否有效和是否为空的区别
      • 不为空的时候,也不一定有效,比如说,一个结构体类型对象,并不是空的情况下,因为没有某个字段,所以找字段是就是无效的
      • 而且并不是所有的类型都可以用IsNil方法的
      • 无效的v除了IsValid、String、Kind之外的方法都会导致panic
  • Value类型的Kind是一个map类型的话,可以通过MapIndex(Value类型的key)这个函数去获取到这个key对于的value

    • 这个Value类型的key怎么实现呢
      • 就是继续用reflect.ValueOf函数去得到一个key的Value类型
  • 结构体通过reflect.ValueOf函数获取到的Value类型,还可以通过FieldByName(字段名)获取到结构体的字段,通过MethodByName(方法名)获取到结构体的方法

  • 反射中的一些操作和反射外的正常操作都不太一样

  • 结构体通过reflect.TypeOf函数获取到Type类型,它的Kind又是一个结构体的话,那么它还有一系列的方法可以获取到结构体的字段和方法的相关信息,方法见下:

    • Field(i int) StructField	
      根据索引,返回索引对应的结构体字段的信息。
      
      ② NumField() int	
      返回结构体成员字段数量。
      
      ③ FieldByName(name string) (StructField, bool)	
      根据给定字符串返回字符串对应的结构体字段的信息。
      
      ④ FieldByIndex(index []int) StructField	
      多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。
      
      ⑤ FieldByNameFunc(match func(string) bool) (StructField,bool)	
      根据传入的匹配函数匹配需要的字段。
      
      ⑥ NumMethod() int	
      返回该类型的方法集中方法的数目
      
      ⑦ Method(int) Method	
      返回该类型方法集中的第i个方法
      
      ⑧ MethodByName(string)(Method, bool)	
      根据方法名返回该类型方法集中的方法
      
  • 上面关于结构体类型的Type具有的一些寻找字段的函数的返回值中有一个类型是StructField类型,这个类型的结构如下:

    • type StructField struct {
          // Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
          // 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
          Name    string
          PkgPath string
          Type      Type      // 字段的类型
          Tag       StructTag // 字段的标签
          Offset    uintptr   // 字段在结构体中的字节偏移量
          Index     []int     // 用于Type.FieldByIndex时的索引切片
          Anonymous bool      // 是否匿名字段
      }
      
    • 这个Tag可能不止一个,那么如何根据tag的名字获取相应tag的值呢

      • 就是先.Tag获取到所有的tag,然后.Get(tag名)方法获取到这个tag名的tag值
  • field就表示字段的意思

  • 有了这些方法啊函数啊什么的,就会发现,这个reflect包是真的可以获取到变量的信息的

  • 这个结构体的字段们和函数们的遍历是没有range去做的,只能先通过一个方法得知数目,然后用一个i去遍历,因为也是有通过i去得到对应字段和方法的方法

  • 结构体反射获取方法的结果是一个Method类型,这个类型的结构可能和字段的结构有点像,总是肯定是有Type和Name字段的

  • 结构体反射获取的方法也是可以调用的

    • 首先这个方法是Type类型去获得的
    • 然后获得后用Call方法去调,这个Call方法的参数是一个 []reflect.Value 类型
    • 然后 []reflect.Value 里面放的就是这个函数的所有参数
  • 反射还是不要滥用啊

    • 反射中的类型错误会引发panic
    • 代码也难以理解
    • 性能也低下
    • 虽然可能比较灵活吧

并发

  • java中是线程实现并发,线程是由操作系统调度的

    • go的并发为什么处理的好
      • 是因为go用goroutine来实现并发,这个是由go的运行时(runtime)调度完成的
      • 也就是说调度和上下文切换的机制由go内置了
  • 宏观上来看,就是把需要并发的内容写在一个函数里面,然后开启一个goroutine去执行这个函数

    • 具体是如何实现的呢
      • 在调用函数的前面加上go关键字,就可以创建一个goroutine去管理这个函数了,效果相当于java中开启了一个线程
  • main函数在启动的时候是默认有一个goroutine去管理它的

    • 而且,当这个main函数中除了goroutine管理的代码外的代码全都执行完成后,这里面goroutine管理的代码也会停止执行
      • 所以会产生一种情况:当main中非并发的代码执行地过快时,这里面并发的代码可能还没执行完就要强制停止了
    • 让当前程序停一停的办法
      • 是用time时间包里面的Sleep函数
        • time包里面还有一些时间变量,比如Second表示1秒
      • 还可以用sync包里面的WaitGroup类型变量的Wait方法去等待所有登记的goroutine结束
        • 这个怎么实现呢
          • 在开启goroutine的时候调用这个类型变量的Add方法(相当于登记+1)
          • 在结束goroutine的时候调用这个类型变量的Done方法(相当于登记-1)
          • 然后在main函数的最后调用这个变量的Wait方法,那么这个方法什么时候会结束呢,就是这个变量的登记为0的时候,也就是所有的goroutine结束的时候
          • 那么很明显这个变量应该是个全局变量,因为函数结束的时候要调用这个变量的Done方法,main函数里面需要调用其Add方法
          • 一些需要
    • 程序去创建goroutine的时候也需要花那么一丢丢丢丢丢的时间
  • goroutine比os线程还有一个优势

    • 就是goroutine的栈内存是可以按需增大和缩小的,最小是2kb,最大是1GB,而线程是固定的2MB
    • 因为这个优势,使得goroutine可以一次性开创出很多,所以说go处理并发要更好一点
  • go自己的调度系统叫做GPM

    • G:goroutine,存放goroutine的信息
    • P:存储goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),同时对自己管理的goroutine队列做调度,当自己管理的队列都消费完时还会去全局队列和其他P队列里面抢任务
      • P管理一组队列,但其实它存放的还是一个goroutine
      • 所以这个的最大数量,可以理解为物理线程数,默认是256(P和M是一一对应的,那么M也就是有最大数量的)
    • M:machine,是对操作系统内核线程的虚拟,goroutine最终要在M上运行
      • P调度这个G去M上运行,当G在一个M上阻塞很久的时候runtime会新建一个M,把P上其余的G调度到M上执行,等到阻塞的G死亡或者阻塞完成时,再回收旧的G
      • 所以可以看出这个P和M也是一个一一对应的关系
  • P的最大数量在并打并发的时候还会增加,但是也不能太多,切换太频繁得不偿失

  • 总结go的并发优势:

    • goroutine是自己调度的
    • 调度技术是m:n,m个goroutine在n个os线程上执行
    • goroutine的调度在用户态下完成,没有用户态和内核太的频繁切换
    • 用户态维护着一片很大的内存池
    • 充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上(调度技术m:n)
    • goroutine很轻量
  • P的最大值是通过GOMAXPROCS来设定的,那么之间的关系是什么呢

    • 2的GOMAXPROCS次方就是P的最大值
  • GOMAXPROCS是什么值呢

    • 是CPU的核心数
  • GOMAXPROCS除了可以决定P的最大值外还可以决定什么呢

    • 可以决定调度技术m:n中的n值,这两个值是相等的
    • 这个n指的就是Go代码可以同时调度上几个OS线程上执行
    • 其实可以发现,背后真正的OS线程并不是很多,这两这个切换不就减少了吗
  • runtime包中的GOMAXPROCS()函数可以设置当前程序并发时占用的CPU逻辑核心数

    • 比如我们把逻辑核心数设为1,那么就是说OS线程数是1,那么goroutine都是在一个线程里面跑
      • 疑问:在这一个线程里面跑的多个goroutine是不是要经过P的调度啊,所以外界看来这些goroutine还是并发的吧?
        • 实验证明:对的
    • 逻辑核心数就是物理OS线程数,当然物理线程数如果越多的话,这个外界看来的并发效果更加明显,就更像是并发
  • 疑问:一个OS线程上是不是也会有多个P和M啊?

  • 疑问:物理OS线程的数量是和CPU核心有关的,那么多个核心是真正并发的吗

  • goroutine和OS线程是多对多的关系,即m:n

    • 疑问:一个goroutine可以跑到多个OS线程上去执行吗
  • 对内存加锁的方式保证数据交换的正确性会降低性能

  • go的并发模型:CSP(Communicating Sequential Processes):通信顺序进程

    • 通过通信共享内存,而不是通过共享内存实现通信
    • 通过通道来实现通讯
  • channel就像是队列,先进先出

    • 通道中的元素是同一类型的元素
    • 通道是来连接不同的goroutine的
    • 是引用类型,如何定义呢
      • var 通道名 chan 通道中元素类型
    • 引用类型的初始化还是要用make函数,不用的话是没有办法直接用的
      • make的第一个参数就是通道类型:chan 通道中元素类型
      • 第二个参数是可选的,通道就相当于一个缓冲区,所以第二个参数就是缓冲区的大小
    • 通道的三个操作
      • 接收:接收数据,入队,是用一个左箭头,接收的数据写在左箭头的右边,通道写在左边,数据指向通道
      • 发送:出队,也是左箭头,通道指向一个接收变量,没有接收变量的时候就是可以忽略结果,这个也是合法的写法
      • 关闭:就是内置函数close,参数是通道
        • 通道只有在接收方goroutine都接收完毕的时候才需要关闭
        • 而且其实不关也可以,因为它会被垃圾回收机制回收,但是文件打开后既定要记住在结束操作时关闭
        • 通道关闭后是不可以继续入队的,会panic,但是可以出队,把通道中剩余的数据给出队读出来
        • 那如果此时通道是空的呢,其实还是会读到东西,只不过是通道中元素类型的零值
        • 通道已经关了再去关一次就panic了
        • 所以这个关闭通道只是关闭了入口而已
  • make通道的时候如果没有定义缓冲区大小,那么这就是一个无缓冲区的通道

    • 这个通道直接入队是不行的,必须先用一个goroutine去接收,然后再去入队

      • 不然会报出deadlock死锁错误

      • 正确实例:

        • func recv(c chan int) {
          	ret := <-c
          	fmt.Println("接收成功", ret)
          }
          func main() {
          	ch := make(chan int)
          	go recv(ch) // 启用goroutine从通道接收值
          	ch <- 10
          	fmt.Println("发送成功")
          }
          
        • 可以发现这个函数的参数直接就是通道,然后函数体中想要获得的数据是从通道这边获取的

          • 疑问:通道中数据只能是一种类型,那要是函数需要多种类型参数的话,怎么办呢
    • 这种类型的通道使得通道数据和goroutine数据同步了,因此也被称为同步通道

  • 有缓冲区的通道

    • 疑问:缓冲区还有cap?
  • 通道出队的返回值是有两个的,第一个是值,第二个是出队成功与否

    • 如果通道已经关闭了的话,那么这个第二个参数就是false
  • 通道也是可以用for range去遍历的,如果通道关闭了话,这个for range就会结束

    • 疑问:那通道没有关闭,也没有数据的时候,这个for range会关闭吗
      • 貌似是这个通道不关,for range循环就不结束,而且更常用这种方式去判断通道是否被关闭
    • 疑问:for range算出队吗(感觉应该算)
  • 单向通道是指通道在一个函数中只能入队或者只能出队,如何实现呢

    • 通道类型本来是写成chan 通道中元素类型
    • chan写成chan<-就是指只能入队
    • 写成<-chan就是指只能出队
  • 通道是作为goroutine管理的函数的参数

  • 函数参数传递:双向通道可以转换为单向通道,但是反过来不行

  • 对于只能入队的单向通道,可以在所有数据入队完成后关闭通道,这并不影响数据的读取

  • 多个处理相同任务的函数并发时,可以设置他们的参数中包含两个分别只能入队和出队的通道,通道是引用类型,这样这几个函数并发时就是处理同一个通道中的数据,也是向同一个通道返回数据,这就实现了内存的共享

  • 并发函数内部通常是需要一个只能出队的通道获取数据,一个只能入队的通道返回结果的

  • worker pool是一种工作模式,就是指定处理相同业务逻辑的函数并发时的goroutine的个数,最简单的实现这种模式的方法就是用循环,比如:

    • // 开启3个goroutine
      	for w := 1; w <= 3; w++ {
      		go worker(w, jobs, results)
      	}
      
  • 用一个变量去接收通道的值时,如果此时通道中没有数据,那么会发生阻塞,也就是说这个变量会一直等下去,直到有数据进来

    • 如果这个通道已经关闭了,那么不会阻塞,而是直接返回类型的零值
  • 如果通达通道中数据已经满了,此时还想入队的话也会阻塞

    • 所以其实入队和出队都有可能发生阻塞
  • 当有多个通道的数据需要处理时,一个一个的写可读性很差,我们可以用select的写法

    • select的写法类似于switch,只不过是case后面的内容不太一样

      • select的case后面写的是通道的接收和发送的语句
      • select也是有default默认操作的
    • 其实一次还是只做一个操作,是从case中选取出一个不用阻塞等待的情况先执行,配合上外层的循环的话,就感觉是哪个通道满足条件就立马执行

    • 当有多个case满足条件没有阻塞时,go会随机选择一个case执行

    • 如果select没有case的话会一直等待,可以用来阻塞mian函数

      • 疑问:阻塞main函数是不是为了让goroutine执行完什么的
        • 那么如何结束这个阻塞呢
    • 疑问:select里面的case没有一个满足条件的,也没有default,那么是不是就什么都不执行结束了呢

  • synchronize是同步的意思,mutex是互斥的意思,在sync包下面有一个Mutex类型,这个类型的Lock和Unlock方法可以为临界区加锁和解锁

    • 临界区应该就是指lock方法被调用后面的代码吧

    • 这个lock就叫做互斥锁

    • 解锁后临界区给哪个goroutine访问是随机的

    • 这个锁住后,其他goroutine读和写的操作都不能进行

  • sync包下面还有一个RWMutex类型,这个类型叫做读写互斥锁,它可以使得临界区被多个goroutine访问却不能修改,也可以使得临界区只能被一个goroutine访问和修改

    • 这个类型的Lock方法同Mutex类型的Lock方法,是一个写锁,只有当前的goroutine可以访问和修改
    • RLock方法就是读锁,这个资源变量可以被很多goroutine读却不能写
    • 这两个方法自然也是有对应的解锁方法
    • 在读的时候加上读锁,让其他goroutine不要改变该goroutine正在读的数据,同理写的时候加上写锁,让其他goroutine也不要去读和写这个数据
    • 当读多写少的场景再用,不然这个优势也发挥不出来
  • 如何表示10ms,10*time.Millisecond,其他时间类似,都是对time包下面时间类型的算术得出来的

    • 如何获取当前时间
      • time包下面的Now函数
      • 返回结果是time包下面的Time类型
    • 如何得到两个时间之间的差值
      • 后面的时间.Sub(前面的时间)
      • time包下面的Time时间类型有Sub方法,可以得到两个Time类型的时间差
      • 返回结果是time包下面的Duration类型,Duration是期间/持续时间的意思
  • 因为main不会等待从main这里开出去的goroutine,为了让goroutine完成,必须要让main函数阻塞,如果用time.Sleep去阻塞明显是不合理的,此时可以用sync包里面的WaitGroup类型来实现

    • 这个类型的Add方法会让自己维护的计数器增加相应数额,写代码时可以每开启一个goroutine就调用这个方法让计数器加一,也可以确定好要开几个goroutine,然后直接调用这个方法把计数器数目加好
    • Done方法可以写在并发函数的defer里面,让这个函数结束时就把计数器-1
    • 最后再main里面需要调用Wait函数,它会等待这个计数器变为0时继续执行其下代码,计数器变为0,也就相当于goroutine结束了,不然的话这行代码会阻塞在这边
    • 这三个函数的接收者都是这个WaitGroup类型的指针,但是我们在外面是可以直接用这个类型的变量去调用的,其实这个一个语法糖
    • WaitGroup类型其实是一个结构体
  • 当并发函数中的某一段代码,比如加载配置文件,只希望它加载一次的时候

    • 之前做法是对配置文件进行判断,看是否被加载,没被加载,就加载
    • 但是当并发时,可能会发生两个goroutine在自己的时间段内都感知到配置文件没有被加载,那么就会重复
    • 学过锁的时候会想到给这个配置文件加一个锁来解决,但是加锁会影响性能
    • 解决方法就是用sync包下面的Once类型的Do方法
      • 这个方法的参数就是只想要执行一次的代码所组成的函数
      • 然后其他的就不用管了,这个代码在函数并发执行的过程中也只会在第一次的时候执行一下
      • 这个作为参数的函数也有参数时,要用闭包
  • go如何实现单例模式,就可以用sync包下面的Once类型的Do方法来完成

    • 一个对象只有一个实例,那么这个对象得先有一个函数可以获取到这个实例,并且获取到的实例永远只有这一个

    • 在这个对象获取其实例的函数中,用一个Once类型的Do方法,这个方法的参数就是这个对象本来正常获取实例的函数(实例的获取以指针的形式)

    • 最后这个对象获取其实例的函数返回这个指针就好

    • 正常初始化的代码只执行一次,每次获取实例都是那一个值

    • 实例:(写的太好了)

      • func GetInstance() *singleton {
            once.Do(func() {
                instance = &singleton{}
            })
            return instance
        }
        
      • 这个instance是共用的

  • sync包下面的Once类型具体是如何实现这个只执行一次的

    • 有一个布尔值来确定参数代码有没有执行过(一般把只需要执行一次的内容称之为初始化操作)
    • 还有一个互斥锁,把初始化的操作保护起来,谁都不能再执行,它自己一直占用
  • fatal是“致命的”的意思;concurrent是“同时进行的,并存的”的意思

  • 开箱即用:对于需要初始化才可以使用的引用类型来说,开箱即用就不需要make去初始化,就可以直接使用,相信这个里面还会包含很多便捷的操作方法

  • 如何不对Map类型加锁就实现Map类型并发时的安全呢

    • 可以用sync中的Map类型来代替原本需要并发处理的Map类型
    • 这个类型的添加元素方式是用其Store存储方法,两个参数,一个key,一个value
    • 获取元素方法是Load加载方法,一个参数key就行
      • 同样还是两个返回结果
    • 这个类型本质也是一个结构体
  • 锁机制的底层是基于原子操作的,一般直接通过CPU指令实现

    • go中原子操作由内置的标准库sync/atomic提供
    • 貌似是有一个包atomic,然后里面有方法,是原子操作类型的
  • 有一种做法还挺聪明的,把互斥锁类型作为资源类型结构体的一个字段

    • 然后在这个结构体的方法中用结构体的锁其限制方法对结构体其他字段的修改
    • 这种做法就是用互斥锁去实现并发安全的常规做法吧
  • 除了上面的做法外,还可以用原子操作去做,这个效率更高,但是吧非特殊情况还是不要用了

  • 复习一下,直接写个for{}就是一个无限的循环

  • for range通道的时候,就一个循环变量就已经表示其中数据了,没有index的概念在里面

    • 使用这个方式去获取通道元素的时候一定要事先保证通道已经关闭了,不然会发生死锁

# Gin

  • Gin用命令行go get -u github.com/gin-gonic/gin下载时会遇到问题

    • go get: module github.com/gin-gonic/gin: Get "https://proxy.golang.org/github.com/gin-gonic/gin/@v/list": dial tcp 216.58.200.49:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
    • 解决方案:(两个命令行命令)
      • go env -w GO111MODULE=on
      • go env -w GOPROXY=https://goproxy.io,direct
    • 然后重新go get -u github.com/gin-gonic/gin
  • gin框架如何实现web的请求与响应呢

    • 首先,东西都在gin包里面

    • 然后需要创建一个路由引擎gin包里面的Default函数

    • 路由引擎有四种API,符合REST风格

      • REST风格就是指(只要满足下面这四点,就可以看成是REST风格,符合RESTful API)

        • 查询的时候用GET
        • 创建的时候用POST
        • 删除的时候用DELETE
        • 更新的时候用PUT
      • API怎么写呢

        • API函数的第一个参数是相对路径

        • 第二个参数是一个函数,当客户端的请求符合请求方式和请求路径时,就执行这个函数

          • 这个函数的格式是固定的,只有一个参数,是gin中Context类型的指针
          • 在这个函数里面,如果想返回一个json数据的话,Context类型有一个JSON方法可以实现
            • 这个方法的第一个参数是响应码
            • 第二个参数是gin包中的H类型
              • 这个类型是一个map
        • 完整代码就是(以GET为例)

          • 	r := gin.Default()	r.GET("/book", func(c *gin.Context) {		c.JSON(200, gin.H{			"message": "GET",		})	})	r.Run(":8080")
            
          • 最终页面上显示的内容就是这个H结构体内容以json格式的输出

        • 最后就是r.Run(),这个就会启动HTTP服务,默认是在0.0.0.0:8080启动服务

          • 可以指定端口r.Run(":8080")
  • gin框架向客户端返回一个页面呢

    • 首先我们需要把页面写好,放在一个文件夹里面,比如templates
      • 疑问:是不是只能在templates里面啊?
    • 然后在main里面,路由引擎创建好后,这个引擎有一个LoadHTMLGlob方法,参数应该是路径,意思是渲染符合路径的所有页面
      • 还有一个方法是LoadHTMLFiles,参数是需要渲染的html文件的相对路径们,这个是指定特定的需要渲染的html文件
    • 然后在那四个API方法里面,第二个参数还是那个格式的函数,但是函数体变了
      • 之前的Context类型的JSON方法要变成HTML方法
        • 第一个参数是http的响应码,可以用的是http包下面的变量(或者是常量)
          • JSON方法中也是可以用的
        • 第二个参数是这个请求跳转到的页面的相对路径(是不包含templates的,是templates后面的路径)
        • 第三个参数还是gin包下面的H类型(本质是一个Map),里面放的是数据,这个数据在html页面中,可以通过{{}}获取到
    • 其实是比json的时候多了一个提前的渲染,其他的逻辑方面没有什么太大问题
  • 没有看自定义模板函数、静态文件处理和使用模板继承这三个地方

  • 获取当前执行程序的路径的办法:

    • func getCurrentPath() string {	if ex, err := os.Executable(); err == nil {		return filepath.Dir(ex)	}	return "./"}
      
  • gin包下面的H类型其实本质是一个Map,其key是string类型,value是interface{}空接口类型,也就是任意类型

    • 而其实这种形式的map就是结构体(严重怀疑结构体的定义就是这么来的)
    • 那么其实除了可以直接定义一个gin.H类型然后赋值之外,我们还可以直接用一个结构体去代替它
  • 没有看XML渲染、YMAL渲染和protobuf渲染这三个地方

  • func(c *gin.Context)的函数体中如何获取c中参数

    • 问号后面的参数如何获取(这个叫querystring)
      • Context类型的Query方法,参数就是key
        • DefaultQuery方法,参数是key和这个key没找到时的默认值
        • 疑问:这个返回结果是什么类型呢?只能是字符串吗,还是什么呢
    • 表单数据如何获取呢
      • Context类型的PostForm方法,参数就是key
        • 同理,存在一个Default方法来添加默认值
    • post方式提交的json数据
      • 这个数据其实是来源于请求体的,我们称之为原始数据
      • json数据其实就是一个json字符串,然后字符串反序列化后是一个结构体
      • 这个是Context类型的GetRawData方法
      • 这个方法是两个返回结果
      • 貌似往往需要错误处理
    • path参数
      • path参数和querystring不一样,querystring是带有问号的哪些,而这个path参数是直接作为path路径的一部分的
      • Context类型的Param方法可以做到
    • 复习一下,json包的反序列化方法是有返回值的
    • 问号后面的参数、表单数据、json数据的方法都不一样,那这就很麻烦,还要区分前端的请求数据是什么格式,而且还要自己把参数封装成实体,很麻烦
      • 所以现在Context类型有一个ShouldBind方法,可以自动识别Context中数据的参数即类型,然后自动绑定到对应的对象上去
      • 需要绑定到的对象就是这个方法的参数
      • 使用这个方法不用区分前端传回数据类型,而且也不用我们自己封装
      • 但是path参数目前是没有办法做到的
      • 这个函数的参数是绑定对象的指针
  • 疑问:结构体字段打tag的时候,有哪些种类的tag呢

    • type Login struct {	User     string `form:"user" json:"user" binding:"required"`	Password string `form:"password" json:"password" binding:"required"`}
      
    • 这上面的form和binding tag是什么意思呢

  • 文件上传这部分没有看

  • 重定向是指内部给你重新指定跳转到了一个url,这个是不会显示先浏览器的网址栏上面的

    • 重定向分为两种

      • http重定向:重定向到一个新的http网址,比如直接跳转到www.baidu.com

        • Context类型的Redirect方法可以实现
          • 第一个参数是http.StatusMovedPermanently
            • 这个是什么意思
          • 第二个参数是新的http网址
      • 路由重定向:只是根地址后面的内容变了而已,比如/test到/test2

        • 两步走

          • 先为Context指定新的url

            • c.Request.URL.Path = "/test2"
              
          • 然后调用gin路由引擎的HandleContect方法,重新路由一下

            • 参数就是更改后的Context

            • r.GET("/test", func(c *gin.Context) {    // 指定重定向的URL    c.Request.URL.Path = "/test2"    r.HandleContext(c)})
              
  • gin路由方式

    • 先创建gin路由引擎

      • r := gin.Default()
        
    • 然后其实r.GET(),r.POST()等等方法就是路由引擎在规划路由

      • 特别的,r.Any()的话任何方式的请求都可以路由到

      • 还可以为不存在的url配置一个路由处理

        • 默认是404

        • 调用的是路由引擎的NoRoute方法,只有一个参数,就是处理函数,里面内容还是正常写

        • 	r.NoRoute(func(c *gin.Context) {		c.HTML(http.StatusNotFound, "views/404.html", nil)	})
          
      • 但这个只是普通的路由,这些规划都是散的,其实可以给一些地址划分成组,这样便于管理

    • 将拥有相同前缀的路由划分为一个路由组,用的是路由引擎的Group方法,参数就是相对前缀路由,这个方法得到的返回结果变量,可以像gin路由引擎一样,正常去使用.GET,.POST等方法,然后这些方法里面的url,是相对于前缀的url

      • 为了看上去美观,可以把这些路由用一个花括号括起来

      • r := gin.Default()	userGroup := r.Group("/user")	{		userGroup.GET("/index", func(c *gin.Context) {...})		userGroup.GET("/login", func(c *gin.Context) {...})		userGroup.POST("/login", func(c *gin.Context) {...})	}	shopGroup := r.Group("/shop")	{		shopGroup.GET("/index", func(c *gin.Context) {...})		shopGroup.GET("/cart", func(c *gin.Context) {...})		shopGroup.POST("/checkout", func(c *gin.Context) {...})	}	r.Run()
        
      • 在路由组里面甚至还可以继续分组,方法同上一层

        • 路由引擎换成是上一层Group方法的结果对象
    • 这个实现是构造了一个路由地址的前缀树


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值