代码改变世界

golang 基础笔记一

2022-02-18 13:54  天心PHP  阅读(36)  评论(0编辑  收藏  举报

1.基本数据类型和数组都是值传递

变量

声明一个变量

第一种,指定变量类型,声明后若不赋值,使用默认值

var name type
name = value

第二种,根据值自行判定变量类型(类型推断Type inference)

如果一个变量有一个初始值,Go将自动能够使用初始值来推断该变量的类型。因此,如果变量具有初始值,则可以省略变量声明中的类型。

var name = value

第三种,省略var, 注意 :=左侧的变量不应该是已经声明过的(多个变量同时声明时,至少保证一个是新变量),否则会导致编译错误(简短声明)

name := value

// 例如
var a int = 10
var b = 10
c : = 10

这种方式它只能被用在函数体内,而不可以用于全局变量的声明与赋值

多变量声明

第一种,以逗号分隔,声明与赋值分开,若不赋值,存在默认值

var name1, name2, name3 type
name1, name2, name3 = v1, v2, v3

第二种,直接赋值,下面的变量类型可以是不同的类型

var name1, name2, name3 = v1, v2, v3

第三种,集合类型

var (
    name1 type1
    name2 type2
)

1.1 布尔型bool

布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true       占用一个字节   不能 用0表示false  1表示true

1.2 数值型

1、整数型

  • int8 有符号 8 位整型 (-128 到 127) 长度:8bit     有符号                                     -2^7 ~  2^7-1              一个字节8位,第一位表示符号正或负

  • int16 有符号 16 位整型 (-32768 到 32767)                                                           -2^15 ~  2^15-1

  • int32 有符号 32 位整型 (-2147483648 到 2147483647)                                       -2^31 ~  2^31-1

  • int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)   -2^63 ~  2^63-1

  • uint8 无符号 8 位整型 (0 到 255) 8位都用于表示数值:  无符号         0 ~ 2^8-1          一个字节8位,第一位也表示数字

  • uint16 无符号 16 位整型 (0 到 65535)                                                 0 ~ 2^16-1

  • uint32 无符号 32 位整型 (0 到 4294967295)                                       0 ~ 2^32-1

  • uint64 无符号 64 位整型 (0 到 18446744073709551615)                   0 ~ 2^64-1

int和uint:根据底层平台,表示32或64位整数。除非需要使用特定大小的整数,否则通常应该使用int来表示整数。 大小:32位系统32位,64位系统64位。 范围:-2147483648到2147483647的32位系统和-9223372036854775808到9223372036854775807的64位系统。

数据类型转换  

var i int32 = 100
var n1 float32 = float32(i)

上面只是把 i 的值转换为 float32 赋值给 n1  但  i 本身类型没有变化

 int64很大的数 转换为 int8  会做溢出处理

2、浮点型

  • float32

    IEEE-754 32位浮点型数

  • float64  (推荐使用)

    IEEE-754 64位浮点型数

  • complex64

    32 位实数和虚数

  • complex128

    64 位实数和虚数

3、其他

  • byte

    类似 uint8

  • rune

    类似 int32

  • uint

    32 或 64 位

  • int

    与 uint 一样大小

  • uintptr

    无符号整型,用于存放一个指针

1.3 字符串型

字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识Unicode文本

var str string
str = "Hello World"
字符串是由字节组成,单个字符可以用byte存储
单引号一个字符/汉字 是对应的 ASCII码值
1.字符串类型为 string,使用双引号或者反引号包起来     
2.字符串拼接 "hello" + "world" 多个链接 + 后面换行

1.4 数据类型转换:Type Convert

语法格式:Type(Value)

常数:在有需要的时候,会自动转型

变量:需要手动转型 T(V)

注意点:兼容类型可以转换

二、 复合类型(派生类型)

1、指针类型(Pointer) 2、数组类型 3、结构化类型(struct) 4、Channel 类型 5、函数类型 6、切片类型 7、接口类型(interface) 8、Map 类型

package main
import (
    "fmt"
    "unsafe"
)
func main()  {
    var n2 int64 = 10
    fmt.Printf("n2 的类型:%T,n2 的占用字节数%d",n2,unsafe.Sizeof(n2))
}

结果

n2 的类型:int64,n2 的占用字节数8

常量(const) 

常量是一个简单值的标识符,在程序运行时,不会被修改的量。

显式类型定义: const b string = "abc"

隐式类型定义: const b = "abc"

package main
import "fmt"
func main() {
   const LENGTH int = 10
   const WIDTH int = 5   
   var area int
   const a, b, c = 1, false, "str" //多重赋值
const d = 9/3 //OK
area = LENGTH * WIDTH fmt.Printf("面积为 : %d", area) println() println(a, b, c) }

常量可以作为枚举,常量组

const (
    Unknown = 0
    Female = 1
    Male = 2
)

常量组中如不指定类型和初始化值,则与上一行非空常量右值相同

package main
import (
    "fmt"
)
func main() {
    const (
        x uint16 = 16
        y
        s = "abc"
        z
    )
    fmt.Printf("%T,%v\n", y, y) //uint16,16
    fmt.Printf("%T,%v\n", z, z) //string,abc
}

常量的注意事项:

1)常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型

2)不曾使用的常量,在编译的时候,是不会报错的

3)显示指定类型的时候,必须确保常量左右值类型一致,需要时可做显示类型转换。这与变量就不一样了,变量是可以是不同的类型值

iota

iota,特殊常量,可以认为是一个可以被编译器修改的常量

iota 可以被用作枚举值:

const (
    a = iota
    b = iota
    c = iota
)

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:

const (
        a = iota //0
        b  //1
        c  //2
    )
    //新的一个则重新开始算
const ( 
        d = iota  //0
        e = iota  //1
        f,g = iota,iota //2,2 //所以需要新的一行才会加1
    )

iota 用法

package main
import "fmt"
func main() {
    const (
            a = iota   //0
            b          //1
            c          //2
            d = "ha"   //独立值,iota += 1
            e          //"ha"   iota += 1
            f = 100    //iota +=1
            g          //100  iota +=1
            h = iota   //7,恢复计数
            i          //8
    )
    fmt.Println(a,b,c,d,e,f,g,h,i) //0 1 2 ha ha 100 100 7 8
}

如果中断iota自增,则必须显式恢复。且后续自增值按行序递增

自增默认是int类型,可以自行进行显示指定类型

数字常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获取地址

三、类型转换

其他类型转换为字符串类型

var num int = 99
var num1 float64 = 56.32
var b bool = true
var str string

str = fmt.Sprintf("%d",num)

str = fmt.Sprint(num)

str = strconv.Itoa(num) 
str = strconv.FormatInt(int64(num),10)

str = strconv.FormatFloat(num1,'f',10,64)

str = strconv.FormatBool(b)
fmt.Printf("str type %T str=%q\n",str,str)

string类型转换为其他类型  用的 strconv包

var str string = "true"
var str1 string = "12"
var str2 string = "56.24"
var b bool
var n1 int64
var n2 int
var f1 float64
b,_ = strconv.ParseBool(str)
fmt.Printf("str type %T str=%v\n",b,b)
n1,_= strconv.ParseInt(str1,10,64)
n2 = int(n1)
fmt.Printf("str type %T str=%v\n",n1,n1)
fmt.Printf("str type %T str=%v\n",n2,n2)
f1,_ = strconv.ParseFloat(str2,64)
fmt.Printf("str type %T str=%v\n",f1,f1)

如果 一个  str ="hello" 转换为int  会转换为默认值 0   如果转换为 bool  也会是默认值  flase

 值类型:

           基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct

引用类型:

         指针、slice 切片、map、管道 chan、interface 等都是引用类型

四、算数运算

1. 除法整除整 取整  要得到小数部分 必须有浮点数据参与运算

fmt.Println(10 /4)     //2 只会取整数部分
fmt.Println(10.0 /4)   //2.5 有浮点数参加  才会有小数部分
fmt.Println(10%-3)    //1  取余(取莫)  按照公式  a % b = a - a / b * b 
GO 只有 num++  num--  没有  ++num  --num  也不允许 a := b++  a:=b-- 只能当作一个独立的使用

不使用第三个变量来交换两个变量的值

//常规做法
var a int = 9
var b int = 5
a = a + b // 9 + 5
b = a - b // a + b - b
a = a - b // a + b - a
fmt.Println(a,b)
//go的做法
a,b = b,a

运算符优先级

 键盘输入语句:

fmt.包的    func Scan   fmt.Scanln    fmt.Scanf  例如 :

var x,y int
fmt.Scanln(&x,&y)

其他进制转十进制

二进制   1101 = 1*2^0 + 0*2^1 + 1*2^2 + 1*2^3 = 1+0+4+8 = 13

八进制   1111 = 1*8^0 + 1*8^1+1*8^2 + 1*8^3 = 1+8+64+512 = 585

十六进制   0x111 = 1*16^0 + 1*16^1+1*16^2 = 1+16+256 = 273

十进制转其他进制   

十进制转二进制    规则:将该数不断除以2,直到商为0为止,然后将每步得到的余数倒过来,就是对应的二进制

转二进制  56 

十进制转八进制    规则:将该数不断除以8,直到商为0为止,然后将每步得到的余数倒过来,就是对应的八进制

十进制转十六进制    规则:将该数不断除以16,直到商为0为止,然后将每步得到的余数倒过来,就是对应的二进制

二进制转八进制  规则:将二进制数每三位一组(从低位开始组合),转换对应的八进制数即可    11 010 101 = 0325

二进制转十六进制  规则:将二进制数每四位一组(从低位开始组合),转换对应的十六进制数即可    1101 0101 = 0xD5

 八进制转二进制  规则:将八进制数每一位,转换成对应的一个3位的二进制即可   0237 = 10011111

十六进制转二进制  规则:将十六进制数每一位,转换成对应的一个4位的二进制即可   0237 = 1000110111

原码、反码、补码

网上对源码,反码,补码的解释过于复杂,我这里精简6局话:

对于有符号的而言:

1)二进制的最高位是符号位:0表示整数,1表示负数

1====>[0000 0001]  -1====>[1000 0001]

2)正数的源码,反码,补码都一样

3)负数的反码 = 它的原码符号位不变,其他位取反(0->1 1->0)

1===>原码[0000 0001] 反码[0000 0001]  补码[0000 0001]

-1===>原码[1000 0001] 反码[1111 1110]  补码[1111 1111]

4)负数的补码 = 他的反码+1

5)0的反码 补码都是0

6)在计算机运算的时候,都是以补码的方式来运算的

1+1  1-1 = 1+(-1)

 

一.switch分支结构

switch 细节讨论

1) case后是一个表达式(即:常量值、变量、一个有返回值的函数等都可以)

2) case后的各个表达式的值的数据类型,必须和switch的表达式数据类型一致

3) case后面可以带多个表达式,使用逗号间隔.比如case 表达式1,表达式2 ...

4) case后面的表达式如果是常量值(字面量),则要求不能重复

5) case后面不需要带brear,程序匹配到一个case后就会执行对应的代码块,然后退出switch,如果一个都匹配不到,则执行default

6) default语句不是必须的

7) switch后也可以不带表达式,类似多个if--else分支来使用

8) switch后也可以直接声明/定义一个变量,分号结束,不推荐

9) switch 穿透-fallthrough,如果在case语句块后增加fallthrough,则会继续执行下一个case,也叫switch穿透

10) Type Switch: switch语句还可以被用于type-switch来判断某个 interface 变量中实际指向的变量类型

var score int = 30
switch {
    case score >90 :
         fmt.Println("成绩及格...")
    case score >70:
         fmt.Println("成绩优良...")
    case score >60 && score<70:
         fmt.Println("成绩及格...")
    default:
        fmt.Println("成绩不及格")
}
//编写一个函数,可以判断输入的参数是什么类型
type Student struct {

}
//编写一个函数,可以判断输入的参数是什么类型
func TypeJudge(items... interface{})  {
    for index,v :=range items{
       index++
      switch v.(type) {
         case bool:
            fmt.Printf("第%v个参数是 bool 类型,值是%v\n",index,v)
         case int32:
            fmt.Printf("第%v个参数是 int32 类型,值是%v\n",index,v)
         case int64:
            fmt.Printf("第%v个参数是 int64 类型,值是%v\n",index,v)
         case float32:
            fmt.Printf("第%v个参数是 float32 类型,值是%v\n",index,v)
         case float64:
            fmt.Printf("第%v个参数是 float64 类型,值是%v\n",index,v)
         case string:
            fmt.Printf("第%v个参数是 string 类型,值是%v\n",index,v)
          case Student:
             fmt.Printf("第%v个参数是 Student 类型,值是%v\n",index,v)
          case *Student:
             fmt.Printf("第%v个参数是 *Student 类型,值是%v\n",index,v)
         default:
            fmt.Printf("第%v个参数是 不确定 类型,值是%v\n",index,v)
      }
   }
}

func main()  {
   var n1 float32 = 1.1
   var n2 float64 = 5.69
   var n3 bool = true
   var n4 int32 = 5
   var n5 int64 = 9
   var n6 string = "hello"
   stu1 :=Student{}
   stu2 :=&Student{}
   TypeJudge(n1,n2,n3,n4,n5,n6,stu1,stu2)
}

二.for循环

//1.第一种
for i:=1;i<=10;i++{
   fmt.Println("输出",i)
}

//2.第二种
j:=1
for j<=10{
   fmt.Println("shuchu",j)
   j++
}

//3.第三种
i:=1
for ; ;  { //也等价于  for{
   fmt.Println(i)
   if i>10{
      break
   }
   i++
}
//遍历字符串
var str string ="hello,world"
1.第一种
for i:=0;i<len(str);i++{
   fmt.Printf("%c\n",str[i])
}
2.第二种
for index,val := range str{
   fmt.Printf("index=%d,val=%c\n",index,val)
}
//如果字符串里面有中文,遍历是取的每一个字节,汉字在utf-8里面占三个字节
var str string ="hello,world北京"
str1 := []rune(str)
for i:=0;i<len(str1);i++{
   fmt.Printf("%c\n",str1[i])
}
//如果要获取str的字符串长度,而不是按字节长度计算 len是按字节长度计算的
fmt.Println("str 的长度:",utf8.RuneCountInString(str))
//通过rune类型处理unicode字符
fmt.Println("str 的长度: rune:",len([]rune(str)))
/*golangbyte数据类型与rune相似,它们都是用来表示字符类型的变量类型。它们的不同在于:

byte 等同于int8,常用来处理ascii字符
rune 等同于int32,常用来处理unicodeutf-8字符*/
//这种方式比较智能,按照字符的方式遍历 有中文也是没问题的 
for i,v :=range str{
  fmt.Printf(
"index=%d,var=%c\n",i,v)
}
//产生一个0-100的随机数,当这个数等于99时 退出
rand.Seed(time.Now().Unix())//随机种子  也可以用 UnixNano() 纳秒
for{
    see := rand.Intn(100)+1 // [1,100)+1
    fmt.Println(see)
    if(see==99){
      break
    }
}
//break 跳出多层循环,指定标签
lable2:
for i:=1;i<4;i++{
  for j:=1;j<10;j++{
    if j==2{
    break lable2
    }
    fmt.Println(j)
  } 
}
//continue跳过本次循环
lable2:
for i:=1;i<4;i++{
   for j:=1;j<10;j++{
      if j==2{
         continue  lable2 //跳出本次循环
      }
      fmt.Println("i=",i,"j=",j)
   }
}
//结果
i= 1 j= 1
i= 2 j= 1
i= 3 j= 1
 

go  build -o bin/my.exe  目录

三.递归函数

package main
import "fmt"
func test(n int){
    if n>2{
        n--
        test(n)
    }
    fmt.Println("n=",n)
}
func main()  {
   test(4)
}
//结果
n = 2 
n = 2
n = 3

//求出第n个斐波拉契数
func fbn(n int)int{
   if n==1 || n==2{
      return 1
   }else {
      return fbn(n-1)+fbn(n-2)
   }
}
func main()  {
   res :=fbn(10)
   fmt.Println(res)
}
已知 f(1)=3 ;f(n) = 2*f(n-1)+1
func fbn(n int)int{
   if n==1{
      return 3
   }else {
      return 2*fbn(n-1)+1
   }
}
func main()  {
   res :=fbn(10)
   fmt.Println(res)
}
//一堆桃子,猴子第一天吃了其中的一半,并再多吃一个,以后每天都吃其中的一半再多吃一个,第十天想吃时,发现只一个了 问:最初共多少个桃子
1.第十天只有一个
2.第九天 (第十天的桃子+1)*2 //第九天吃完就只一个   第九天N个   n/2-1 = 第十天的桃子  那么  n= (第十天的桃子+1)*2
3.规律 第N天的桃子  peach(n) = (peach(n+1)+1)*2
func peach(n int)int{
   if n>10 || n<1{
      fmt.Println("输入的天数不对")
      return 0
   }
   if n==10{
      return 1
   }else {
      return (peach(n+1)+1)*2
   }
}
func main()  {
   res :=peach(1)
   fmt.Println(res) //1534
}

四.函数

1.在go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了,通过该变量可以函数调用

2.函数既然是一种数据类型,因此在GO中,函数可以作为形参,并调用

3.为了简化数据类型定义,GO支持自定义数据类型

   基本语法:type 自定义数据类型  数据类型 //理解:相当于一个别名

   案例  type  myInt int //这时myInt 就等价 int 来使用了   //但是  myInt 和 int 是两种不同的数据类型

   案例  type  mySum  func(int,int)int //这时mySum就等价 一个函数类型 func(int,int)int

type myFunType func(int,int)int //定义函数类型
func getSum(n1,n2 int)int{
   return n1+n2
}
func myFun(funcvar myFunType,num1 int,num2 int)int  {
   return funcvar(num1,num2)
}
func main()  {
   a := getSum
   fmt.Printf("a的类型%T,getSum类型是%T\n",a,getSum)
   res :=a(10,100)
   fmt.Println("res= ",res)

   res1 := myFun(getSum,50,39)
   fmt.Println("res1=",res1)
   
   type myInt int
   var num1 myInt
   var num2  int
   num1 = 10
   num2 = int(num1)//需要转换
   fmt.Println(num1,num2)

}

4.支持对函数返回值命名

func getSum(n1 int,n2 int)(sum int,sub int)  {
    sum = n1+n2
    sub = n1-n2
    return
}
func main()  {
   res,res1 := getSum(100,23)
   fmt.Println(res,res1)
}

5.GO支持可变参数    可变参数必须放到形参的最后面

func getSum(args ...int)int {
  sum :=0
  for i:=0;i<len(args);i++{
      sum+=args[i]
  }
  return sum
}
func main()  {
   res := getSum(1,2,3,4,5,6)
   fmt.Println(res)
}

6.每一个源文件都可以包含一个init函数  该函数会在mian函数执行前,被GO运行框架调用,也就是说 init会在main函数前被调用   通常完成初始化工作

  如果一个文件同时包含全局变量,init函数 main函数 ,执行的顺序   全局变量定义 》init > mian

var age = test()
func test()int  {
    fmt.Println("test()...")
    return  90
}
func init()  {
    fmt.Println("init()...")
}
func main()  {
    fmt.Println("mian()...age=",age)
}
结果:
test()...
init()...
main()...

对于init 函数

如果 package 存在依赖,调用顺序为最后被依赖的最先被初始化,例如:导入顺序 main –> A –> B –> C,则初始化顺序为 C –> B –> A –> main,一次执行对应的 init 方法。main 包总是被最后一个初始化,因为它总是依赖别的包

五.若冥函数(闭包)

匿名函数

var(
    fun1 = func(n1 int,n2 int)int {//全局匿名函数
        return  n1*n2
    }
)

func main()  {
    res := func(n1 int,n2 int)int { //匿名函数
        return  n1+n2
    }(10,20)
    fmt.Println(res)

    a := func(n1 int,n2 int)int { //匿名函数赋值给变量
        return n1 - n2
    }
    re1 := a(10,3)
    fmt.Println(re1)

    re2 :=fun1(2,3)
    fmt.Println(re2)
}

闭包:就是一个函数和与其相关的引用环境组合的一个整体

func addUpper() func(int)int  {
    var n int = 10
    return func(x int) int {
        n = n+x
        return n
    }
}
func main()  {
    f := addUpper()
    fmt.Println(f(1)) //11
    fmt.Println(f(2)) //13
        fmt.Println(f(3)) //16

}
//上述 n 只初始化一次,多次调用 就在累加  返回的匿名函数和 n 构成了闭包
func makeSuffix(suffix string) func(string)string  {
    return func(name string) string {
        if !strings.HasSuffix(name,suffix){ //是否存在指定后缀
            return name + suffix
        }
        return name
    }
}
func main()  {
    f := makeSuffix(".jpg")
    fmt.Println("文件名处理后:",f("winter"))
}

六.defer的使用 (延时机制)

压入栈,先进后出的原则,如果涉及到相关的值,也会拷贝同时入栈

func sum(n1 int,n2 int)int  {
    defer fmt.Println("ok1 n1=",n1)  //压入栈  先进后出
    defer fmt.Println("ok2 n2=",n2) //压入栈  先进后出
        n1++
        n2++
    res :=n1+n2
    fmt.Println("ok3 res=",res)
    return res

}
func main()  {
    res := sum(10,20)
    fmt.Println(res)
}
//
ok3 res= 32
ok2 n2= 20
ok1 n1= 10
32

全局变量  只能用 var  a int    不能用   a : = 10   相当于  var  a  int   和   a = 10 .   a = 10  赋值必须放在函数体里面

1.查找子串是否在指定的字符串中  :strings.Contains("seafood","food")  //true

2.10进制转换 2 ,8 ,16  :str = strconv.FromatInt(123,2)  // 2->8->16

3.统计一个字符串有几个指定的字串: strings.Count("ceheese","e") //4

4.不区分大小写字符串比较(==是区分字母大小写的):strings.EqualFold("abc","Abc") //true

5.返回子字符串第一次出现的位置 :strings.Index("NLT_abc","abc") //4

6.返回子串在字符串最后一次出现的位置  如果没用返回-1  :strings.LastIndex("go golang","go")  

7.将指定的字符串替换成,另外一个字串:strings.Replace("go go hello ","go","go 语言",n)  //n 可以指定你希望替换几个,如果n=-1 表示全部替换

8.按照制度的木个字符,为分割标识,将一个字符串拆分为字符串数组 : strings.Split("hello,world,ok",",")

9.j将字符串的字母进行大小写转换 :strings.ToLower("GO")// go   strings.ToUpper("go") // GO

10.将字符串左右两边的空格去掉: strings.TrimSpace(" tn a lone gopher ntm   ")

11.将字符串左右两边指定的字符去掉:strings.Trim("!hello!","!")   可以去掉多个  strings.Trim("!hello!"," !") //将左右两边的空格和! 去掉

12.将字符串左边指定的字符串去掉:strings.TrimLeft("!hello!","!")

13.将字符串右边指定的字符串去掉:strings.TrimReft("!hello!","!")

14.判断字符串是否以指定的字符串开头:strings.HasPrefix("ftp://192.168.10.1","ftp")//true

15.判断字符串是否以指定的字符串结束:strings.HasSuffix("NLT_abc.jpg","abc")// false

七.时间日期函数

1.获取当前时间:time.Now()  //类型 time.Time

2.获取日期  

now :=time.Now()
年:now.Year() 月:int(now.Month()) 日:now.Day() 时:now.Hour() 分:now.Minute() 秒:now.Second()

3.格式化日期

now :=time.Now()
//第一种
fmt.Printf("当前日期: %d-%d-%d %d:%d:%d\n",now.Year(),now.Month(),now.Day(),now.Hour(),now.Minute(),now.Second()) //当前日期: 2022-2-9 16:30:58
dateStr := fmt.Sprintf("当前日期: %d-%d-%d %d:%d:%d\n",now.Year(),now.Month(),now.Day(),now.Hour(),now.Minute(),now.Second())
fmt.Println(dateStr) //当前日期: 2022-2-9 16:32:41
//第二种
//2006/01/02 15:04:05  日期数字是固定的
fmt.Printf(now.Format("2006/01/02 15:04:05")) //2022/02/09 16:34:15

1天=24小时 = 24*60 分钟 = 24*60*60秒 = 24*60*60*1000毫秒 = 24*60*60*1000*1000 微秒 = 24*60*60*1000*1000*1000 纳秒

second 秒  Millisecond 毫秒  Microsecond 微秒  Nanosecond 纳秒   unix时间戳  unixnano 纳秒时间戳
time.Sleep(time.Second) //睡眠一秒
time.Sleep(time.Millisecond*100) // 毫秒*100  每隔0.1秒
now.Unix() //时间戳
now.UnixNano() //纳秒时间戳
//测试程序执行时间
func  test(){
    str := ""
    for i:=0;i<100000;i++ {
        str +="hello"+strconv.Itoa(i)
    }
}
func main()  {
    statr := time.Now().Unix()
    test()
    end := time.Now().Unix()
    fmt.Printf("执行时间为:%v",end-statr)
}

八.内置函数 buildin

len() 用来求长度,比如 string,arry,slice,map,channel
cap() 用来求容量 比如 slice
new() 用来分配内存,主要用来分配值类型,比如int,float32,struce ...返回的是指针
make() 用来分配内存的,主要用来分配引用类型,比如 chan,map,slice.
num2 := new(int)
fmt.Printf("num2的类型%T,num2的值=%v,num2的地址%v,num2这个指针,指向的值=%v",num2,num2,&num2,*num2)
//结果
num2的类型*int,num2的值=0xc00000a0a0,num2的地址0xc000006028,num2这个指针,指向的值=0

九.错误机制

1.在默认情况下,当发生错误后(panic),程序就会退出(崩溃)

2.GO中引入的处理方式为:defer,panic,recover

3.这几个异常的使用场景可以这么简述:GO中可以抛出一个panic 的异常,然后在defer中通过recover捕获这个异常,然后正常处理

func test()  {
    defer func() {
        err := recover() //recover 内置函数,可以捕获到异常
        if err !=nil{ //说明捕获到错误
            fmt.Println("err=",err)
        }
    }()
    num1 := 10
    num2 := 0
    res := num1/num2  //此处发生错误  引发panic
    fmt.Println("res=",res)
}
func main()  {
    test()
    fmt.Println("main()下面代码继续执行")
}

4.自定义错误

1.GO中也支持自定义错误,使用errors.New 和 panic内置函数

  1)errors.New(”错误说明“),会返回一个error类型的值,表示一个错误

  2)panic内置函数,接收一个interface{} 类型的值(空接口可以接受任何值)作为参数,可以接受error类型的变量  输出错误信息,并退出程序

func readconf(name string) error  {
    if name=="config.ini"{
        //正确的
        return nil
    }else {
        //返回一个自定义错误
        return errors.New("读取文件错误")
    }
}
func test()  {
    err :=readconf("config.ini")
    if err !=nil{
        panic(err)
    }
    fmt.Println("test()继续执行")
}
func main()  {
    test()
    fmt.Println("main()下面代码继续执行")
}

十.数组

1.数组可以存放多个同一类型的数据,数组也是一种数据类型,在GO中数组是值类型

数组的定义
var  数组名  [数组的大小]数据类型
var  a [5]int
赋值  a[0] =1 a[1]=2

数组的四种定义方式
var arr1 [3]int = [3]int{1,2,3} //[1 2 3]
var arr2 = [3]int{3,4,5} //[3 4 5]
var arr3 = [...]int{7,8,9} //[7 8 9]
var arr4 = [...]int{1:200,2:400,4:500} //[0 200 400 0 500]

1.数组的地址,也就是数组的第一个元素的地址 (数组是连续分配内存的)
var arr [3]int
fmt.Printf("数组的地址%p,数组的第一个地址%p",&arr,&arr[0])
//结果
数组的地址0xc000010360,数组的第一个地址0xc000010360

2.数组地址大小间隔 由数组的类型决定 int 间隔8

数组循环

var arr = [5]string{"张三","李四","王五","张二麻子","田小雨"}
for i:=0; i<len(arr);i++ {
   fmt.Println(arr[i])
}
for index , value := range arr{
   fmt.Printf("下标是:%v,值是:%v\n",index,value)
}

1.数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的,不能动态变化

2.var  arr  []int  这时arr是一个slice切片

3.数组中的元素可以是任何数据类型,包括值类型,和引用类型 但不能混用

4.数组创建后如果没用赋值,有默认值,数值类型为0  字符串类型为”“  bool类型为 false

5.使用数组的步骤:1声明数组并开辟孔明,2 给数组各个元素赋值 3 使用数组

6.数组下标必须在指定范围内使用,否则报panic  比如 var  arr [5]int  有效的下标 0-4

7.数组默认情况值传递,会进行值拷贝

8.如果想在其他函数修改数组中的值 ,可以使用引用传递(指针方式)

//生成五个随机数,将其反转
rand.Seed(time.Now().UnixNano())
var intArr [5]int
var arrlen int = len(intArr)
for i:=0;i<arrlen;i++{
  intArr[i] = rand.Intn(100)
}
fmt.Println(intArr)
tem :=0
for i:=0;i<arrlen/2;i++{
  tem = intArr[arrlen-1-i]
  intArr[arrlen-1-i] = intArr[i]
  intArr[i] = tem
}
fmt.Println(intArr)

十一.切片

1.切片是数组的引用,是一个引用类型

a := [5]int{76, 77, 78, 79, 80}
var b []int = a[1:4] //creates a slice from a[1] to a[3]
fmt.Println(b) //[77 78 79]  长度len(b) //3 容量cap(b) //4

修改切片:slice没有自己的任何数据。它只是底层数组的一个表示。对slice所做的任何修改都将反映在底层数组中。

slice 从底层来说 其实就是一个数据结构(struct 结构体)

type  slice struct{
  ptr  *[2]int
  len int
  cap int
}

切片的使用 

方式1.定义一个切片,然后让切片去引用一个已经创建好的数组,比如上面的案例

var arr = [5]int{1,2,3,4,5}
var slice = arr[1:]  //[2 3 4 5]
var slice1 = arr[:3] //[1 2 3]
var slice2 = arr[:]  //[1,2,3,4,5]
//切片可以继续切片
var newslice = slice2[1:4]  //[2 3 4]

方式2.通过make来创建切片,var  切片名 []type = make([]int,len,cap)   参数说明:type 就是数据类型  len 大小   cap 容量  

var  slice []int = make([]int,4,10)
fmt.Println(slice)  //[0 0 0 0]
fmt.Println("slice len=",len(slice),"slice cap=",cap(slice))  //slice len= 4 slice cap= 10
slice[1] = 10
slice[3] = 20
fmt.Println(slice)  //[0 10 0 20]

 make创建的切片  操作的数组对外部不可见

方式3.定义一个切片,直接就指定具体数组,使用原理类似make的方式

var slice []string = []string{"tom","jack","mary"}
fmt.Println(slice)

append 动态增加切片

var slice []int = []int{100,200,300}
slice = append(slice,400,500,600)
fmt.Println(slice) //[100 200 300 400 500 600]

slice = append(slice,slice...) //将后面的slice的元素被打散一个个append进strss
fmt.Println(slice) // [100 200 300 400 500 600 100 200 300 400 500 600

切片拷贝 copy 

1.拷贝数据是相互独立的   

2.必须都是切片类型 

3.如果拷贝容量不够,则只会拷贝相应容量大小的

var slice []int = []int{1,2,3,4,5}
var slice1 = make([]int,10) //如果此处的容量不够  则只会拷贝当前容量的值    make([]int,2) //[1 2] 会被拷贝进来 
copy(slice1,slice)
fmt.Println(slice) // [1 2 3 4 5]
fmt.Println(slice1) // [1 2 3 4 5 0 0 0 0 0]

string 和 slice

1.string底层是一个byte数组,因此string也可以进行切片处理

str :="hello@world"
slice :=str[6:]
fmt.Println(slice) //world

2.string 是不可变的,也就是说不能通过 str[0] = 'z' 方式来修改字符串

3.如果需要修改字符串,可以先将string ->[]byte 或者 []rune ->修改->重写转成 string.

str :="hello@world"
str1 := []byte(str)
str1[0] = 'z'
str = string(str1)
fmt.Println(str)
//如果有中文则 转换成 []rune   rune按字符处理的 兼容中文
str1 := []rune(str)
str1[0] = ''
str = string(str1)
fmt.Println(str) //倍ello@world

将一个斐波拉契放到一个切片

func fbn(n int)[]uint64  {
    fbnslice := make([]uint64,n)
    fbnslice[0] = 1
    fbnslice[1] = 1
    for i := 2;i<n;i++ {
        fbnslice[i] = fbnslice[i-1]+fbnslice[i-2]
    }
    return fbnslice
}
func main()  {
   res :=fbn(10)
   fmt.Println(res) // [1 1 2 3 5 8 13 21 34 55]
} 

十二.排序查找

//冒泡排序 【24 69 80 57 13】
1.一共会经过arr.length-1次的轮数比较,每一轮将会确定一个数的位置
2.每一轮的比较次数在逐渐减少
3.当发现前面的一个数比后面的一个数大的时候,就进行交换
//冒泡排序
func bubbleSort(arr *[10]int)  {
   fmt.Println("排序前的数组:",*arr)
   for i:=0;i<len(*arr)-1;i++{
      for j:=0;j<len(*arr)-1-i;j++{
         if((*arr)[j]>(*arr)[j+1]){
            (*arr)[j],(*arr)[j+1] = (*arr)[j+1],(*arr)[j]
         }
      }
   }
   fmt.Println("排序后的数组:",*arr)
}
func main()  {
   arr :=[10]int{3,1,4,5,6,7,8,9,10,11}
   bubbleSort(&arr)
}
//冒泡排序优化1
假设我们现在排序ar[]={1,2,3,4,5,6,7,8,10,9}这组数据,按照上面的排序方式,第一趟排序后将10和9交换已经有序,接下来的8趟排序就是多余的,什么也没做。所以我们可以在交换的地方加一个标记,如果那一趟排序没有交换元素,说明这组数据已经有序,不用再继续下去。
func bubbleSort1(arr *[10]int)  {
   fmt.Println("排序前的数组:",*arr)
   for i:=0;i<len(*arr)-1;i++{
      flag := true //定义标识
      for j:=0;j<len(*arr)-1-i;j++{
         if((*arr)[j]>(*arr)[j+1]){
            (*arr)[j],(*arr)[j+1] = (*arr)[j+1],(*arr)[j]
            flag = false //说明还在发生数据交换
         }
      }
      if flag{ //当内循环没有发生了数据交换  说明已经排好了
         break
      }
   }
   fmt.Println("排序后的数组:",*arr)
}
func main()  {
   arr :=[10]int{1,2,3,4,5,6,7,8,10,9}
   bubbleSort1(&arr)
}
//冒泡排序优化2
我们可以继续优化。既我们可以记下最后一次交换的位置,后边没有交换,必然是有序的,然后下一次排序从第一个比较到上次记录的位置结束即可。

func bubbleSort2(arr *[10]int)  {//优化
   fmt.Println("排序前的数组:",*arr)
   last_pos :=len(*arr)-1 //记录每一次外部循环过程中,最后进行数据交换的位置
    next_pos :=len(*arr) //记录每一次数据交换的位置
   for i:=0;i<len(*arr)-1;i++{
      flag := true
      for j:=0;j<last_pos;j++{
         if((*arr)[j]>(*arr)[j+1]){
            (*arr)[j],(*arr)[j+1] = (*arr)[j+1],(*arr)[j]
            flag = false
            next_pos = j //交换元素,记录最后一次交换的位置
         }
      }
      if flag{
         break
      }
      last_pos = next_pos
   }
   fmt.Println("排序后的数组:",*arr)
}
func main(){
   arr1 :=[10]int{1,2,5,7,4,3,6,8,9,10}
   bubbleSort2(&arr1)
}

//冒泡排序优化3
优化二的效率有很大的提升,还有一种优化方法可以继续提高效率。大致思想就是一次排序可以确定两个值,正向扫描找到最大值交换到最后,反向扫描找到最小值交换到最前面。例如:排序数据1,2,3,4,5,6,0


func bubbleSort3(arr *[10]int){
    fmt.Println("排序前的数组:",*arr)
    last_pos :=len(*arr)-1 //记录每一次外部循环过程中,最后进行数据交换的位置
    next_pos :=len(*arr) //记录每一次数据交换的位置
        n := 0 //同时找最大值的最小需要两个下标遍历
    for i:=0;i<len(*arr)-1;i++{
        flag := true
        //正向寻找最大值
        for j:=n;j<last_pos;j++{  
            if((*arr)[j]>(*arr)[j+1]){
                (*arr)[j],(*arr)[j+1] = (*arr)[j+1],(*arr)[j]
                flag = false //加入标记
                next_pos = j //交换元素,记录最后一次交换的位置
            }
        }
        if flag{
           break
        }
        last_pos = next_pos
        for j:=last_pos;j>n;j--{
            if (*arr)[j] < (*arr)[j - 1]{
                (*arr)[j],(*arr)[j - 1] = (*arr)[j-1],(*arr)[j]
                flag = false //加入标记
            }
        }
        n++   //n++是在之后的循环中从第n+1个数开始。比如你第一次循环找出了最大数和最小数,那么第二次循环的时候,你就只要从第2个数和第n-1个数之间去查找最大数和最小数了
        if flag{
           break
        }
    }
    fmt.Println("排序后的数组:",*arr)
}
func main()  {
   arr1 :=[10]int{1,2,5,7,4,3,6,8,9,10}
   bubbleSort3(&arr1)
}

 查找

1.顺序查找

func main()  {
    names :=[4]string{"张三","李四","王五","朱六"}
    var heroName = ""
    fmt.Println("请输入要查找的人名:")
    fmt.Scanln(&heroName)
    index :=-1
    for i:=0;i<len(names);i++{
        if names[i] == heroName{
            index = i
            break
        }
    }
    if index!=-1{
        fmt.Printf("找到%v,下标是%v\n",heroName,index)
    }
}

二分查找 (必须有序)

func BinaryFind(arr *[10]int,leftIndex int,rightIndex int,findVal int){
    if leftIndex>rightIndex{
        fmt.Println("找不到")
        return
    }
    //先找到中间的下标
    middle := (leftIndex + rightIndex) / 2
    if(*arr)[middle] >findVal{
        BinaryFind(arr,leftIndex,middle-1,findVal)
    }else if (*arr)[middle]<findVal{
        BinaryFind(arr,middle+1,rightIndex,findVal)
    }else {
        fmt.Println("找到了,下标为",middle)
    }

}
func main()  {
    arr :=[10]int{1,2,3,4,5,6,7,8,9,10}
    BinaryFind(&arr,0,len(arr)-1,5)
}

十三.二维数组

func main()  {
    var arr [4][6]int  //arr :=[4][6]int{{1,2,3,4,5,6},{7,8,9,10,11,12},{13,14,15,16,17,18},{19,20,21,22,23,24}}
    arr[1][2] = 1
    arr[2][1] = 2
    arr[2][3] = 3
    for i:=0;i<len(arr);i++{
        for j:=0;j<len(arr[i]);j++{
            fmt.Print(arr[i][j]," ")
        }
        fmt.Println()
    }
}

for _,v :=range arr{
   for _,v1 :=range v{
      fmt.Print(v1," ")
   }
   fmt.Println()
}
var arr [2][3]int  // var arr [...][3]int
fmt.Println(arr)
fmt.Printf("第一个地址:%p\n",&arr[0]) //0xc0000b6060
fmt.Printf("第二个地址:%p\n",&arr[1]) //0xc0000b6078   //这个是16进制  比第一个相差24   因为每个里面都有3个元素int(8)  3*8=24
这个二维数组 是 两个指针分别指向一个3位的一维数组

//求出三个班,每个班的五个学生的,每个班级的平均分和所有班级的平均分
func main()  {
   rand.Seed(time.Now().UnixNano())
   var scores [3][5]float64
   for i:=0;i<len(scores);i++{
      for j:=0;j<len(scores[i]);j++{
           scores[i][j] = float64(rand.Intn(100)+1)
      }
   }
   totalSum := 0.0
   num := 0
   for i:=0;i<len(scores);i++{
      sum :=0.0
      for j:=0;j<len(scores[i]);j++{
         sum += scores[i][j]
         num++
      }
      totalSum +=sum
      fmt.Printf("第%v个班的总分为%v,平均分为:%v\n",i+1,sum,sum/float64(len(scores[i])))
   }
   fmt.Printf("总分为%v,总平均分为:%.2f\n",totalSum,totalSum/float64(num))
}

十四.map

var 变量名  map[keytype][valtype]   map是无序的

key 可以是 bool,数字,string ,指针,channel  还可以是只包含前面几个类型  接口,结构体,数组  通常的为 int ,string

注意:slice,map,还有function 不可以,因为这几个没办法用 == 来判断

value 通常为 整型 浮点型,string , map struct

声明不会分配内存,只有make才会分配内存

func main()  {
   //方式一
   var a map[string]string
   a =make(map[string]string,10)
   a["a"] = "张三"
   a["b"] = "李四"
   a["c"] = "王五"
   fmt.Println(a)
   //方式二
   var b = make(map[string]string)
   b["no1"] = "天津"
   b["no2"] = "bejing"
   fmt.Println(b)
   //方式三
   var heroes map[string]string = map[string]string{
      "her01":"松江",
      "her02":"张飞",
   }
   fmt.Println(heroes)
   heroes1 := map[string]string{
      "her01":"松江",
      "her02":"张飞",
   }
   heroes1["her03"] = "飞刀"
   fmt.Println(heroes)
   fmt.Println(heroes1)
}
//存放三个学生的name和sex
arr1 :=make(map[string]map[string]string)
arr1["stu01"] = make(map[string]string)
arr1["stu01"]["name"] = "tom"
arr1["stu01"]["sex"] = ""
arr1["stu02"] = make(map[string]string)
arr1["stu02"]["name"] = "tom1"
arr1["stu02"]["sex"] = ""
arr1["stu03"] = make(map[string]string)
arr1["stu03"]["name"] = "tom2"
arr1["stu03"]["sex"] = ""
fmt.Println(len(arr1)) //获取map的长度
fmt.Println(arr1)  
//map的遍历一般使用for-range
for key,val:=range arr1{
   fmt.Println(key)
   for k,v :=range val{
      fmt.Printf("\tk=:%v,v=:%v\n",k,v)
   }
}

map的删除操作:delete(map,"key") delete 是一个内置函数,如果key存在,就删除该key-value, 如果不存在,不操作,也是不会报错

arr := make(map[string]string)
arr["nan1"] = "张三"
arr["nan2"] = "李四"
//删除
delete(arr,"nan1") //直接删除
delete(arr,"nan4") //不会报错
fmt.Println(arr)

//查找
val,ok :=arr["nan2"]
if ok{ //true false
   fmt.Println("nan2存在,他的值为%v",val)
}
fmt.Println("没有找到nan2这个key")

//遍历
for key,val:=range arr{
   fmt.Printf("key=:%v,val=:%v\n",key,val)
}

删除map所有key

1.遍历一下key,逐个删除  2.map=make(...),make一个新的,让原来的成为垃圾,被GC回收

map切片 :切片里面放的是map类型 

切片的数据类型如果是map,则我们称为 slice of map ,map切片,这样使用则map个数就可以动态变化了

func main()  {
      monsters :=make([]map[string]string,2) //这是一个切片,里面放的map 切片需要make
      if monsters[0] == nil{
          monsters[0] = make(map[string]string,2) //map 也需要make
        monsters[0]["name"] ="小老鼠"
        monsters[0]["age"] = "400"
      }
    if monsters[1] == nil{
        monsters[1] = make(map[string]string,2) //map 也需要make
        monsters[1]["name"] ="牛魔王"
        monsters[1]["age"] = "500"
    }
    //下面代码错误 切片长度为2 不能赋值第三个会报错 的用切片的append
    /*if monsters[2] == nil{
        monsters[2] = make(map[string]string,2) //map 也需要make
        monsters[2]["name"] ="蜈蚣精"
        monsters[2]["age"] = "200"
    }*/
    newMonsters := map[string]string{ //先创建一个map
        "name":"玉兔精",
        "age": "230",
    }
    monsters = append(monsters,newMonsters)//切片添加
    fmt.Println(monsters)
}

map是无序的,如果需要排序,可以进行key排序,再根据key输出  map排序

func main()  {
   map1 :=make(map[int]string,10)
   map1[10] = "张三"
   map1[1] = "李四"
   map1[4] = "王五"
   map1[8] = "王二麻子"
   fmt.Println(map1)
   var keys []int
   for k,_ :=range map1{
          keys = append(keys,k)
   }
   sort.Ints(keys) //字符串排序  sort.Strings(keys)
   fmt.Println(keys)
   for _,v :=range keys{
      fmt.Printf("map[%v]=%v\n",v,map1[v])
   }
}

1.map是引用类型,遵守引用类型传递的机制,在一个函数接受map,修改后,会直接修改原来的map

func modify(map1 map[int]int)  {
    map1[10] = 900
}
func main()  {
    map1 :=make(map[int]int)
    map1[0] = 3
    map1[5] = 4
    map1[10] = 10
    modify(map1)
    fmt.Println(map1)
}

2.map的容量达到后,再想map增加元素,会自动扩容,并不会发生panic,也就是说map 能动态的增长键值对

3.map的value 也经常使用struct类型,更适合管理复杂的数据(比value是一个map更好)比如value为student结构体

type stu struct {
   Name string
   Age  int
   Address string
}
func main()  {
   students :=make(map[string]stu,10)
   stu1 := stu{"张三",18,"北京"}
   stu2 := stu{"李四",30,"上海"}
   students["students1"] = stu1
   students["students2"] = stu2
   fmt.Println(students)
   for i,v:=range students{
      fmt.Printf("学生的编号%v,学生的姓名:%v,学生的年龄:%v,学生的地址:%v\n",i,v.Name,v.Age,v.Address)
   }
}

十五.结构体,struct

type Cat struct {
   Name string
   Age  int
   Color string
   Hobby string
}
func main()  {
   cat1 := Cat{"小白",18,"白色","吃鱼"}
   cat1.Name ="小白白"
   cat1.Age = 19
   fmt.Printf("名称:%v,年龄:%v,颜色:%v",cat1.Name,cat1.Age,cat1.Color)
}

type Person struct {
    Name string
    Age  int
    Scores [5]float64
    ptr *int
    slice []int
    map1  map[int]int
}
func main()  {
    var p1 Person
    fmt.Println(p1)
    p1.slice = make([]int,10)
    p1.map1 = make(map[int]int)
    fmt.Println(p1)
}

创建结构体的四种类型

type Person struct {
    Name string
    Age  int
}
func main()  {
    //第一种
    var p1 Person
    p1.Name = "张三"
    p1.Age = 20
    //第二种
    p2 := Person{"李四",13}
    fmt.Print(p2)
    //第三中
    var p3 *Person = new(Person)
    (*p3).Name = "王五"  //等价与   p3.Name = "王五"
    (*p3).Age = 30  //等价与   p3.Age = 30

    p3.Name = "和三"
    p3.Age = 50
    //第四种
    var p4 *Person = &Person{"mary",33}  //等价于   var p4 = &Person{"mary",33} //类型推导  p4 := &Person{"mary",33}
    (*p4).Name = "李飞"  //等价于  p4.Name = "李飞"
    p4.Age = 40
    fmt.Println(p4)           
        
}

1.结构体在内存中的字段是连续的

type Point struct {
   x int
   y int
}
type Rect struct {
   leftUp,rightDown Point
}
type Rect2 struct {
   leftUp,rightDown *Point
}
func main()  {
     r1 := Rect{Point{1,2},Point{3,4}}
     fmt.Printf("r1.leftUp.x 地址=%p r1.leftUp.y 地址=%p r1.rightDown.x 地址=%p r1.rightDown.y 地址=%p \n",
       &r1.leftUp.x,&r1.leftUp.y,&r1.rightDown.x,&r1.rightDown.y)//16进制每个都相差8
     //r2 有两个 *Point类型,这两个*Poin类型的本身地址也是连续的,
     //但他们指向的地址不一定是连续的
     r2 := Rect2{&Point{10,20},&Point{30,40}}
   fmt.Printf("r2.leftUp.x 地址=%p r2.leftUp.y 地址=%p r2.rightDown.x 地址=%p r2.rightDown.y 地址=%p \n",
      &r2.leftUp.x,&r2.leftUp.y,&r2.rightDown.x,&r2.rightDown.y)//16进制每个都相差8
    fmt.Printf("r2.leftUp 指向地址=%p ,r2.rightDown 指向地址=%p \n",r2.leftUp,r2.rightDown)
}

 2.结构体是用户单独定义的类型,和其他类型进行转换时需要有完全相同的字段(名称,个数,类型)

3.结构体进行type重新定义(相当于取别名)golang认为是新的数据类型,但是相互间可以强转

4.struct 的每个字段上,可以写一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化   //用户接口请求返回参数

返回结构体,结构体一般大写开头,(也不能改成小写,别的包 要用)很多不适应,我们就用tag 反射机制改成小写

type Monster struct {
    Name string  `json:"name"`
    Age int  `json:"age"`
    Skill string  `json:"skill"`
}
func main()  {
    moster :=Monster{"张三",46,"打人"}
    //将moster序列号
    jsonMoster,err :=json.Marshal(moster)
    if err!=nil {
        fmt.Println("json 处理错误:",err)
    }
    fmt.Println(string(jsonMoster))//{"name":"张三","age":46,"skill":"打人"}
}

十六.结构体,方法

1.方法是作用在指定的数据类型上的,和指定的数据类型绑定,因此自定义类型,都可以有方法,而不仅仅是struct

type Person struct {
    Name string
}
func (p Person) test()  {
        p.Name = "jack"
    fmt.Println("test()...",a.Name)
}
func main()  {
   var p Person
   p.Name = "tom"
   p.test()
   fmt.Println("main() p.name=",p.Name)  //输出 tom 作用域不同,结构体是值类型
}
//1.test方法和Person类型绑定
//2.test方法只能通过 Person类型的变量来调用,而不能直接调用,也不能通过其他类型的变量来调用
//3.func (p Person)test(){....} p表示哪个Person 变量调用
type integer int

func (i integer) print()  {
    fmt.Println("i = ",i)
}
func (i *integer) change(){
    *i = *i + 1
}
func main()  {
    var i integer = 10
    i.print()
    i.change()
    fmt.Println("i = ",i) //11
}

//求圆的面积

type Circle struct {
   Radius float64
}
func (c Circle) area() float64 {
   return 3.14 * c.Radius * c.Radius
}
func (c *Circle) area2() float64  {
   c.Radius = 10
   return 3.14 * c.Radius * c.Radius
}
func main()  {
    var c Circle
    c.Radius = 4.0
    res := c.area() //这里的C 传的是指针,结构体的指针
    fmt.Println("面积为:",res)

    res2 :=c.area2()
    fmt.Println("面积=",res2)
    fmt.Println("c.radius = ",c.Radius) //10 因为是引用传递
}

如果一个类型实现了 String()这个方法,那么fmt.Println  fmt.Print 默认会调用这个变量的 String()进行输出

type student struct {
    Name string
    Age  int
}

func (stu *student) String() string  {
    str := fmt.Sprintf("Name=[%v],Age=[%v]\n",stu.Name,stu.Age)
    return str
}
func main()  {
    stu := student{
        Name: "张三",
        Age:  90,
    }
    fmt.Println(&stu)
}

//写一个加减乘除

type Calcuator struct {
    Num1 float64
    Num2 float64
    op  byte
}

func (ca *Calcuator) Getnum()float64  {
    res :=0.0
    switch ca.op {
    case '+':
        res = ca.Num1+ca.Num2
    case '-':
        res = ca.Num1-ca.Num2
    case '*':
        res = ca.Num1*ca.Num2
    case '/':
        res = ca.Num1/ca.Num2
    default:
        fmt.Println("输入的操作符有误")
    }
    return res
}
func main()  {
    var mn Calcuator
    mn.Num1 = 3.45
    mn.Num2 = 5.89
    mn.op = '/'
    res := mn.Getnum()
    fmt.Printf("结果为:%.2f",res)
}

//函数和方法的区别

1.调用方式不一样

   函数的调用方式 :函数名(实参列表)

   变量.方法名(实参列表)

2.对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然

3.对于方法(如struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样可以  //因为方法底层做了处理

   方法的绑定是值类型, 即使用地址调用  也是值拷贝  例如:(&p).test()

   1.不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和那个类型绑定

   2.如果是和值类型 比如 (p Person)则是值拷贝,如果是和指针类型,比如是(p *Person) 则是地址拷贝

type Person struct {
    Name string
}

func (p Person) test()  {
    p.Name = "jack"
    fmt.Println("test() = ",p.Name)
}
func (p *Person) test1()  {
    p.Name = "make"
    fmt.Println("test() = ",p.Name)
}
func main()  {
   p := Person{Name:"tom"}
   p.test() //test() =  jack
   (&p).test() //test()=jack  //这里仍然是值拷贝
   fmt.Println("man() p.name=",p.Name) //man() p.name= tom
    (p).test1()//test() =  make  等价于  p.test()
}

十七.工厂模式

golang的结构体没有构造函数,通常可以使用工厂模式来解决这个问题

//一个结构体的声明是这样的:
package model
type Student struct{
  Name string
}
因为这里的Student的 首字母S是大写的,如果我们想在其他包创建Student的实例(比如main包)
引入model包后,就可以直接创建Student结构体的变量(实例)
但问题来了,如果首字母是小写的,比如 type student struct{} 就不行了,怎么解决。。。。。。。工厂模式解决

正常大写情况引入包

//packgo/model/student.go
package model
type Student struct {
   Name string
   Score float64
} 

//packgo/main/mian.go
package main

import (
   "fmt"
   "packago/model"
   )
func main()  {
  var stu = model.Student{Name:"张三",Score:58.5}
  fmt.Println(stu) //{张三 58.5}
}

工厂模式

//packgo/model/student.go
package model
type student struct {
   Name string
   score float64
}
//因为student首字母s是小写,只能在model 包用
func NewStudent(n string,s float64) *student  {
   return &student{
      Name:  n,
      score: s,
   }
}
//如果score的首字母小写
func (s *student) GetScore()float64  {
   return s.score
}
//packgo/main/mian.go 
package main

package main

import (
   "fmt"
   "packago/model"
)
func main()  {
   var stu = model.NewStudent("tom",88.8)
   fmt.Println(*stu)//{tom 88.8} 因为score的首字母小写 外部包  不可访问
   fmt.Println("name=",stu.Name,"score=",stu.GetScore()) //name =  tom score= 88.8
}

十八.封装,继承,多重继承

1.将结构体,字段的首字母小写,其他包不能使用,类似private

2.给结构体所在包提供一个工厂模式的函数,首字母大写,类似一个构造函数

3.提供一个首字母大写的Set方法,类似其他语言的public 用于对属性判断并赋值

4.提供一个首字母大写的Get方法,类似其他语言的public 用于获取属性的值

//package\model\preson.go
package model
import "fmt"
type person struct {
   Name string
   age int  //其他包不能访问
   sal float64 //其他包不能访问
}

func NewPerson(name string) *person {
   return &person{
      Name: name,
   }
}
func (p *person) SetAge(age int)  {
   if age>0 && age <150{
      p.age = age
   }else {
      fmt.Println("年龄范围不正确")
   }
}
func (p *person) GetAge()int  {
   return p.age
}
func (p *person) SetSal(sal float64)  {
    if sal>=3000 && sal<30000{
       p.sal = sal
   }else {
      fmt.Println("薪水范围不正确")
   }
}
func (p *person) GetSal() float64  {
   return p.sal
}

//package\mian\mian.go
package main

import (
   "fmt"
   "packago/model"
)

func main()  {
   p := model.NewPerson("smith")
   p.SetAge(18)
   p.SetSal(5000)
   fmt.Println(p) //&{smith 18 5000}
   fmt.Println(p.Name) //smith
   fmt.Println("age=",p.GetAge(),"sal=",p.GetSal()) //age= 18 sal= 5000
}

//继承  解决代码复用

1.当多个结构体存在相同的属性,和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法

其他结构体不需要重新定义这些属性和方法,只需要嵌套一个匿名结构体即可,

golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承的特性

type Strudent struct { //同有特性
   Name string //学生姓名
   Age int  //学生年龄
   Score int //学生成绩
}

func (stu *Strudent) ShowInfo() { //显示学生成绩  同有特性
  fmt.Printf("学生姓名:%v,学生年龄:%v,学生成绩:%v\n",stu.Name,stu.Age,stu.Score)
}
func (stu *Strudent) SetScore(score int){//设置学生成绩  同有特性
   stu.Score = score
}
func (stu *Strudent) GetSum(n1 int,n2 int)int  {
   return n1+n2
}
//小学生
type Pupil struct {
   Strudent  //嵌入了匿名结构体
}
func (p *Pupil) testing()  { //特有的
   fmt.Println("小学生正在考试.......")
}
//大学生
type Graduate struct {
   Strudent  //嵌入了匿名结构体
}

func (p *Graduate)testing()  {//特有的
   fmt.Println("大学生正在考试.......")
}
func main()  {
   pupil := &Pupil{}
   pupil.Strudent.Name = "tom.."
   pupil.Strudent.Age = 8
   pupil.testing()
   pupil.Strudent.SetScore(78)
   pupil.Strudent.ShowInfo()
   fmt.Println("小学生做加法:",pupil.Strudent.GetSum(8,5))

   graduate := &Graduate{}
   graduate.Strudent.Name = "may.."
   graduate.Strudent.Age = 20
   graduate.testing()
   graduate.Strudent.SetScore(99)
   graduate.Strudent.ShowInfo()
   fmt.Println("大学生做加法:",graduate.Strudent.GetSum(80,50))
}

1.结构体可以使用嵌套匿名结构体的所有字段和方法,即:首字母大写或者小写的字段和方法都可以使用

2.匿名结构体字段访问可以简化

3.当结构体和匿名结构体有相同的字段或者方法时,编辑器采用 就近访问原则,如希望访问匿名结构体的字段和方法,可以通过匿名结构体来区分

type A struct {
    Name string
    age int
}
func (a *A)SayOK()  {
    fmt.Println("A SayOk",a.Name)
}
func (a *A)hello()  {
    fmt.Println("A hello",a.Name)
}
type B struct {
    A
     Name string
}

func main()  {
    var b B
        b.Name = "may"
    b.A.Name = "tome"  
    b.A.age = 19    //等价于 b.age = 19
    b.A.hello()  //等价于  b.hello()
    b.A.SayOK()  //等价于  b.SayOK()
}

  (1)当我们直接通过b 访问字段或者方法时,其执行流程如下,比如b.Name

   (2)编辑器会先看b 对应的类型有没有Name,如果有,则直接调用B类型的Name字段

 (3)如果没用就去看B中嵌套的结构A 有没有这个Name 字段 如果有就调用,如果没用 继续找A结构体里面的嵌套

4.结构体嵌入两个或者多个匿名结构体,如两个匿名结构体有相同的字段和方法(同事结构体本身没用同名的字段和方法)

在访问时,就必须明确指定匿名结构体名字,否则编辑报错

type A struct {
    Name string
    Age int
}

func (a *A)Show(){
    fmt.Println("A 的Name=",a.Name,"A 的Age=",a.Age)
}
type B struct {
    Name string
    Age int
}
type C struct {
    A
    B
}
func main()  {
    var c C
    c.A.Name = "tom"
    c.B.Name = "msy"
    c.A.Age = 59
    c.B.Age = 10
    c.Show()
}

5.如果一个struct 嵌套了一个有名的结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

type D struct{
 a A
}
//调用
var d D
d.a.Name = "jack"

6.嵌套匿名结构体后,也可以在创建结构体变量时,直接指定各个匿名结构体的字段值。下面是多重继承,尽量不用使用多重继承

type Goods struct {
   Name string
   Price float64
}
type Brand struct {
   Name string
   Address string
}
type Tv struct {
   Goods
   Brand
}
type Tv1 struct {
   *Goods
   *Brand
}

func main()  {
   var tv =Tv{
      Goods{Name:"电视机",Price:5999},
      Brand{"华为","山东"},
   }
   fmt.Println(tv)

   var tv1 =&Tv1{
      &Goods{Name:"电视机",Price:5999},
      &Brand{"华为","山东"},
   }
   fmt.Println(*tv1.Goods,*tv1.Brand)
}

7.结构体里面也可以匿名基本数据类型

type A struct {
    Name string
    Age int
}
type B struct {
    A
    int
}
func main()  {
    var b B
    b.int = 9
    b.Name = "tom"
    b.Age = 85
    fmt.Println(b)
}

十九.接口 interface

interface 类型可以定义一组方法,但是这些方法不需要实现,并且interface不能包含任何变量,到莫个自定义类型,比如结构体,要使用的时候,在根据具体情况把这些方法写出来

1.接口里面所有方法都没有方法体,即接口的方法都是没用实现的方法。接口体现了程序设计的多态和高内聚低耦合的思想

2.GOlang中接口,不需要显式的实现,只要一个变量,含有接口类型中的所有方法,那么这个变量九实现了这个接口,作为的鸭子类型,长的像鸭子,会下蛋,会鸭叫,会游泳的 就鸭子

type Usb interface {
    Start()
    Stop()
}
type Phone struct {

}
//让Phone 实现 Usb接口的方法
func (p Phone)Start()  {
    fmt.Println("手机开始工作....")
}
func (p Phone)Stop()  {
    fmt.Println("手机停止工作")
}

type Camera struct {

}
func (p Camera)Start()  {
    fmt.Println("相机开始工作....")
}
func (p Camera)Stop()  {
    fmt.Println("相机停止工作")
}
//计算机
type Computer struct {

}
//只要是USB接口  都可以可以用 需要实现USB接口的所有方法
func (c Computer)Working(usb Usb)  {  //这里体现了多态  usb变量会根据传入的实参,来判断到底是Phone 还是Camera
    usb.Start()
    usb.Stop()
}
func main()  {
    computer :=Computer{}//电脑
    phone :=Phone{}//手机
    camera :=Camera{}//相机
    computer.Working(phone)
    computer.Working(camera)
}

1.接口本身不能创建实例,但可以指向一个实现了该接口的自定义类型的变量(实例)

type Ainterface interface {
   Say()
}
type Binterface interface {
   Hello()
}
type Stu struct {
   Name string
}
func (stu Stu)Say()  {
   fmt.Println("stu Say()....")
}
type integer int
//普通类型实现接口
func (i integer)Say()  {
   fmt.Println("integer Say().......")
}

type Monster struct {

}
//Monster 实现 B接口
func (m Monster)Hello()  {
   fmt.Println("Monster Hello()....")
}
//Monster 实现A接口
func (m Monster)Say()  {
   fmt.Println("Monster Say()......")
}
func main()  {
    var stu Stu
    var a Ainterface= stu
    a.Say()
    //普通类型实现接口
    var i integer = 10
    var b Ainterface = i
    b.Say()

    //Monster 实现了 A 和 B 两个接口
    var monster Monster
    var a1 Ainterface = monster
    var a2 Binterface = monster
    a1.Say()
    a2.Hello()
}

2.接口中所有方法都没用方法体,即都没有实现的方法

3.在golang中 一个自定义类型需要将木个接口所有的方法都实现,我们说这个自定义类型实现了该接口

4.一个自定义类型只有实现了木个接口,才能将该自定义类型的实例(变量)赋值给接口类型

5.只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型

6.一个自定义类型可以实现多个接口

7.一个接口(比如A),可以继承多个别的接口(比如B,C)这时如果要实现A接口,也必须将B,C接口的方法也全部实现,但B和C 不能有相同的方法名

type Binterface interface {
    test01()
}
type Cinterface interface {
    test02()
}
type Ainterface interface {
    Binterface
    Cinterface
    test03()
}
type Stu struct {

}
func (stu Stu)test01()  {
   fmt.Println("Binterface test01()......")
}
func (stu Stu)test02()  {
    fmt.Println("Cinterface test02()......")
}
func (stu Stu)test03()  {
    fmt.Println("Ainterface test03()......")
}
func main()  {
   var stu Stu
   var a Ainterface = stu
   a.test02()
}

8. interface类型默认是一个指针,(引用类型),如果没用对interface初始化就使用,那么会输出nil

9.空接口interface{}没有任何方法,所以所有类型都实现了空接口,可以把任何类型的变量赋值给空接口

结构体切片排序

//实现对Hero结构体切片的排序:sort.Sort(data Interface)    sort.Ints() 对一个int切片进行排序  (实用对学生成绩排序)

func main()  {
   slice := []int{1,5,7,3,4,6,88,22,55}
   sort.Ints(slice)//对int类型的切片进行排序
   fmt.Println(slice)
}
package main

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

type Hero struct {
    Name string
    Age int
    Score float64
}
//声明一个Hero结构体切片类型
type HeroSlice []Hero   //切片里面的val 是Hero结构体

//实现Interface接口
func (hs HeroSlice) Len() int{
    return len(hs)
}
//less方法就是决定你使用什么标准进行排序
//1.按hero的年龄从小到大排序
func (hs HeroSlice) Less(i,j int) bool{
    //return hs[i].Age > hs[j].Age //对Age排序
    //return hs[i].Name > hs[j].Name //对Name排序
    return hs[i].Score > hs[j].Score //对Name排序
}
func (hs HeroSlice)Swap(i,j int)  {
    hs[i],hs[j] = hs[j],hs[i]
}
func main()  {
     var heroes HeroSlice
    for i:=0;i<=10;i++ {
        hero :=Hero{
            Name: fmt.Sprintf("硬汉~%d",rand.Intn(100)),
            Age:  rand.Intn(100),
            Score: float64(rand.Intn(100)),
        }
        heroes = append(heroes,hero)
    }
    fmt.Println("排序前:",heroes)
    //调用sort.Sort
    sort.Sort(heroes) //用这个排序必须实现接口,实现接口里面的三个方法,Len(),Less(),Swap(),
    fmt.Println("排序后:",heroes)
}

//对学生成绩排序

package main

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

type Student struct {
    Name string //学生姓名
    Age int  //学生年龄
    Score float64 //学生分数
}

type Stuslice []Student  //结构体切片->切片里面放的结构体

func (stu Stuslice)Len()int  {
    return len(stu)
}
func (stu Stuslice)Less(i,j int)bool  {
    return stu[i].Score>stu[j].Score
}
func (stu Stuslice)Swap(i,j int)  {
    stu[i],stu[j] = stu[j],stu[i]
}
func main()  {
    //结构体切片排序
    var stuslic Stuslice
    for i:=0;i<=10;i++ {
        hero :=Student{
            Name:  fmt.Sprintf("张三~%d",rand.Intn(100)),
            Age:   rand.Intn(100),
            Score: float64(rand.Intn(100)),
        }
        stuslic = append(stuslic,hero)
    }
    fmt.Println("排序前",stuslic)
    sort.Sort(stuslic)
    fmt.Println("排序后",stuslic)

    //整型切片排序
    var slic []int
    for i:=0;i<=10;i++{
        s :=rand.Intn(100)
        slic = append(slic,s)
    }
    fmt.Println(slic)
    sort.Ints(slic)
    fmt.Println(slic)
}

//一个小猴子,继承老猴子的爬树,又要有鸟的飞行能力,又要有鱼的游泳能力

package main

import "fmt"

//定义一个猴子的结构体
type Monkey struct {
    Name string //猴子名称
}
func (mon *Monkey)Climbing()  {
    fmt.Println(mon.Name,"生来会爬树....")
}
//小猴子结构体
type LittleMonkey struct {
    Monkey //小猴子继承了猴子的结构体
}
//鸟的飞行接口
type BridAble interface {
    Flying() //飞行能力
}
//小猴子实现飞行接口
func (mon *LittleMonkey)Flying()  {
    fmt.Println(mon.Name,"通过学习会飞行....")
}
//鱼游泳的接口
type FishAble interface {
    Swimming()
}
//小猴子实现游泳接口
func (mon *LittleMonkey)Swimming()  {
    fmt.Println(mon.Name,"通过学习会游泳....")
}
func main(){
    mokey := LittleMonkey{
        Monkey{
            Name:"悟空",
        },
    }
    fmt.Printf("mokey的类型是:%T\n",mokey)
    mokey.Climbing()
    mokey.Flying()
    mokey.Swimming()
}

1)当A结构体继承了B结构体,那么A结构体就自动继承了B结构体的字段和方法,并且可以直接使用

2)当A结构体需要扩展功能,同时又不希望去破坏继承关系,则可以去实现木个接口即可,因此我们可以认为:实现接口是对继承机制的补充

1.接口和继承解决的问题不同

继承的价值在于:解决代码的复用性和可维护性

接口的价值在于:设计,设计好各种规范(方法),让其他自定义类型去实现这些方法

2.接口比继承更加灵活

接口比继承更加灵活,继承是满足is-a的关系,而接口是满足like-a的关系

3.接口在一定程度上实现代码解耦

二十.多态

1.变量(实例)具有多种形态,面向对象的第三大特征,在GO语言,多态的特征是通过接口实现的,可以按照统一的接口来调用不同的实现,这时接口变量就呈现不同的形态

2.前面的Usb接口案例,Usb usb既可以接受手机变量,又可以接受相机变量,就体现了Usb接口的多态特性

 1)多态参数

  在前面的Usb接口案例,Usb usb 既可以接受手机变量,又可以接受相机变量,就体现了Usb接口多态

2)多态数组

演示一个案例:给Usb数组中,存放Phone结构体,和 Camera结构体变量,Phone还有一个特有的方法call() 请遍历Usb数组,如果是Phone变量,除了调用Usb接口声明的方法外,还需要调用Phone 特有方法Call

package main

import "fmt"

type Usb interface {
    Start()
    Stop()
}
type Phone struct {
    Name string
}

//让Phone 实现 Usb接口的方法
func (p Phone) Start() {
    fmt.Println("手机开始工作....")
}
func (p Phone) Stop() {
    fmt.Println("手机停止工作")
}
func (p Phone) Call() {
    fmt.Println("手机正在打电话....")
}

type Camera struct {
    Name string
}

//让Camera 实现 Usb接口的方法
func (p Camera) Start() {
    fmt.Println("相机开始工作....")
}
func (p Camera) Stop() {
    fmt.Println("相机停止工作")
}

//计算机
type Computer struct {
}

//只要是USB接口  都可以可以用 需要实现USB接口的所有方法
func (c Computer) Working(usb Usb) { //这里体现了多态  usb变量会根据传入的实参,来判断到底是Phone 还是Camera
    usb.Start()
    //如果 usb 是指向 Phone 结构体变量,则还需要调用Call方法
    //类型断言...[注意体会!!!]
    if phone, ok := usb.(Phone); ok {
        phone.Call()
    }
    usb.Stop()
}
func main() {
    //定义一个Usb接口数组,可以存放Phone 和 Camear 的结构体变量
    //这里就体现出了多态数组
    var usbArr [3]Usb
    usbArr[0] = Phone{Name: "vivo"}
    usbArr[1] = Phone{Name: "小米"}
    usbArr[2] = Camera{Name: "尼康"}
    //遍历 usbArr
    //Phone 还有一个特有的方法 call(),请遍历 Usb数组,如果是 Phone 变量,
    //除了调用 Usb 接口声明的方法外 还需要调用 Phone 特有方法 call=>类型断言
    var computer Computer
    for _, v := range usbArr {
        computer.Working(v)
        fmt.Println()
    }
}

结果

手机开始工作....
手机正在打电话....
手机停止工作

手机开始工作....
手机正在打电话....
手机停止工作

相机开始工作....
相机停止工作

 

二十一.类型断言

type Point struct {
    x int
    y int
}

func main()  {
    var a interface{}
    var point Point = Point{1,2}
    a = point // 空接口可以接受任何类型的数据
    var b Point
    //b = a  错误的 空接口赋值给结构体
    b = a.(Point)  //类型断言
    fmt.Println(b) //{1 2}
}

类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言。

func main()  {
    var t float64 = 2.25
    var x interface{}
    x = t //接口指向了 float64
    y := x.(float64)
    fmt.Printf("y 的类型是 %T 值是=%v",y,y)
}

 对于上面代码

在进行类型断言时,如果类型不匹配,就会报panic 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型

如果在断言时,带上了检测机制,如果成功就OK,否则也不要报panic

//第一个
func main()  {
    var t float64 = 2.25
    var x interface{}
    x = t //接口指向了 float64
    y,ok := x.(float64)
    if ok{
        fmt.Println("断言成功")
    }else {
        fmt.Println("断言类型不对")
    }
    fmt.Printf("y 的类型是 %T 值是=%v",y,y)
}
//第二个
type Stu struct {
   Name string
   Age int
}
func main()  {
   stu := Stu{Name:"tom",Age:59}
   var a interface{}
   a = stu
    var stu1 Stu
   stu1 = a.(Stu)
   fmt.Println(stu1)
}

给Phone结构体增加一个特有的方法call() 当Usb接口接受的是Phone 变量时,还需要调用call方法

type Usb interface {
    Start()
    Stop()
}
type Phone struct {
    Name string
}
//让Phone 实现 Usb接口的方法
func (p Phone)Start()  {
    fmt.Println(p.Name,"手机开始工作....")
}
func (p Phone)Stop()  {
    fmt.Println(p.Name,"手机停止工作")
}
func (p Phone)Call()  {
    fmt.Println(p.Name,"手机在打电话")
}
type Camera struct {
    Name string
}
func (p Camera)Start()  {
    fmt.Println(p.Name,"相机开始工作....")
}
func (p Camera)Stop()  {
    fmt.Println(p.Name,"相机停止工作")
}
//计算机
type Computer struct {

}
//只要是USB接口  都可以可以用 需要实现USB接口的所有方法
func (c Computer)Working(usb Usb)  {  //这里体现了多态  usb变量会根据传入的实参,来判断到底是Phone 还是Camera
    usb.Start()
    //类型断言
    if phone,ok := usb.(Phone);ok{
        phone.Call()
    }
    usb.Stop()
}
func main()  {
    //定义一个Usb接口数组,可以存放Phone 和 Camear 的结构体变量
    //这里就体现出了多态数组
    var usbArr [3]Usb
    usbArr[0] = Phone{Name:"华为"}
    usbArr[1] = Phone{Name:"小米"}
    usbArr[2] = Camera{Name:"佳能"}
    //遍历数组
    var computer Computer
    for _,v := range usbArr{
        computer.Working(v)
        fmt.Println()
    }
    fmt.Println(usbArr) //[{华为} {小米} {佳能}]
}
//编写一个函数,可以判断输入的参数是什么类型
type Student struct {

}
//编写一个函数,可以判断输入的参数是什么类型
func TypeJudge(items... interface{})  {
    for index,v :=range items{
       index++
      switch v.(type) {
         case bool:
            fmt.Printf("第%v个参数是 bool 类型,值是%v\n",index,v)
         case int32:
            fmt.Printf("第%v个参数是 int32 类型,值是%v\n",index,v)
         case int64:
            fmt.Printf("第%v个参数是 int64 类型,值是%v\n",index,v)
         case float32:
            fmt.Printf("第%v个参数是 float32 类型,值是%v\n",index,v)
         case float64:
            fmt.Printf("第%v个参数是 float64 类型,值是%v\n",index,v)
         case string:
            fmt.Printf("第%v个参数是 string 类型,值是%v\n",index,v)
          case Student:
             fmt.Printf("第%v个参数是 Student 类型,值是%v\n",index,v)
          case *Student:
             fmt.Printf("第%v个参数是 *Student 类型,值是%v\n",index,v)
         default:
            fmt.Printf("第%v个参数是 不确定 类型,值是%v\n",index,v)
      }
   }
}

func main()  {
   var n1 float32 = 1.1
   var n2 float64 = 5.69
   var n3 bool = true
   var n4 int32 = 5
   var n5 int64 = 9
   var n6 string = "hello"
   stu1 :=Student{}
   stu2 :=&Student{}
   TypeJudge(n1,n2,n3,n4,n5,n6,stu1,stu2)
}

二十二.文件操作

流:数据在数据源(文件)和程序(内存)之间经历的路径

输入流:数据从数据源(文件)到程序(内存)的路径

输出流:数据从程序(内存)到数据源(文件)的路径

os.File 封装了所有文件相关操作,File是一个结构体

func main()  {
     //打开文件
     file,err := os.Open("c:/ebay.txt")
     if err != nil{
         fmt.Println("打开文件失败 err=",err)
     }
     fmt.Printf("file=%v",*file)
     //关闭文件
     err = file.Close()
     if err != nil{
         fmt.Println("关闭文件失败 err=",err)
     }
}

1)读取文件的内容并显示在终端(带缓冲区的方式),使用 os.Open   file.Close,  bufio.NewReader(), reader.ReadString 

func main()  {
     //打开文件
     file,err := os.Open("c:/go/golang.txt")
     if err != nil{
         fmt.Println("打开文件失败 err=",err)
     }
     defer file.Close()  ////关闭文件 否则会有内存泄漏

     reader := bufio.NewReader(file) //缓冲读取 默认大小 4096
     //循环读取文件内容
     for{
         str,err := reader.ReadString('\n')
         if err == io.EOF{ //io.EOF 表示文件的末尾
           break
        }
        fmt.Print(str)
     }
     fmt.Println("文件读取结束")
}

2)读取文件的内容并显示在终端,(使用ioutil一次将整个文件读入到内存中),这种方式适用于文件不大的情况,相关方法和函数(ioutil.ReadFile)

func main()  {
    file :="c:/go/golang.txt"
    content,err :=ioutil.ReadFile(file)//返回的是一个byte切片
    if err != nil{
        fmt.Println("读取文件失败,err=",err)
    }
    fmt.Printf("%v",string(content))
}

打开文件的模式

const (
    O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件
    O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件
    O_RDWR   int = syscall.O_RDWR   // 读写模式打开文件
    O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部
    O_CREATE int = syscall.O_CREAT  // 如果不存在将创建一个新文件
    O_EXCL   int = syscall.O_EXCL   // 和O_CREATE配合使用,文件必须不存在
    O_SYNC   int = syscall.O_SYNC   // 打开文件用于同步I/O
    O_TRUNC  int = syscall.O_TRUNC  // 如果可能,打开时清空文件
)
func OpenFile(name string, flag int, perm FileMode) (file *File, err error)  
OpenFile是一个更一般性的文件打开函数,大多数调用者都应用Open或Create代替本函数。它会使用指定的选项(如O_RDONLY等)、指定的模式(如0666等)打开指定名称的文件。如果操作成功,返回的文件对象可用于I/O。如果出错,错误底层类型是*PathError。
name:文件路径 fiag: 上述模式 perm:权限 FileMode 在WINDS下面无效,是用户Linux下面的
const (
    // 单字符是被String方法用于格式化的属性缩写。
    ModeDir        FileMode = 1 << (32 - 1 - iota) // d: 目录
    ModeAppend                                     // a: 只能写入,且只能写入到末尾
    ModeExclusive                                  // l: 用于执行
    ModeTemporary                                  // T: 临时文件(非备份文件)
    ModeSymlink                                    // L: 符号链接(不是快捷方式文件)
    ModeDevice                                     // D: 设备
    ModeNamedPipe                                  // p: 命名管道(FIFO)
    ModeSocket                                     // S: Unix域socket
    ModeSetuid                                     // u: 表示文件具有其创建者用户id权限
    ModeSetgid                                     // g: 表示文件具有其创建者组id的权限
    ModeCharDevice                                 // c: 字符设备,需已设置ModeDevice
    ModeSticky                                     // t: 只有root/创建者能删除/移动文件
    // 覆盖所有类型位(用于通过&获取类型位),对普通文件,所有这些位都不应被设置
    ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice
    ModePerm FileMode = 0777 // 覆盖所有Unix权限位(用于通过&获取类型位)
)

1.创建一个新文件,写入内容 5句"hello,Gardon"

2.打开一个存在的文件中,将来源的内容覆盖成新的内容10句"你好,牛魔王"

3.打开一个存在文件,在原来的内容追加内容"ABC!ENGLIST!"

4.打开一个文件,读出里面的内容,并追加十句"你好,北京"

func main()  {
   filepath :="c:/go/golang1.txt"
   //file,err :=os.OpenFile(filepath,os.O_WRONLY|os.O_CREATE,0666) //1.写 和 创建 的模式
   //file,err :=os.OpenFile(filepath,os.O_WRONLY|os.O_TRUNC,0666) //2.写 和 清空 的模式
   //file,err :=os.OpenFile(filepath,os.O_WRONLY|os.O_APPEND,0666) //3.写 和 追加 的模式
   file,err :=os.OpenFile(filepath,os.O_RDWR|os.O_APPEND,0666) //.读写 和 追加 的模式
   if err != nil{
      fmt.Printf("打开文件错误,err=",err)
   }
   //及时关闭file句柄
   defer file.Close()
   reader :=bufio.NewReader(file)
   for {
      str,err := reader.ReadString('\n')
      if err == io.EOF{
         break
      }
      fmt.Print(str)
   }
   //str :="hello,Gardon\r\n"
   //str :="你好,牛魔王\r\n"
   //str :="ABC!ENGLIST!"
   str :="你好,北京\r\n"

   //写入时,使用带缓存的 *Writer
   writer := bufio.NewWriter(file)
   for i:=0;i<5;i++{
      writer.WriteString(str)
   }
   //因为writer是带缓存,因此在调用writerString方法时,其实内容是写入了缓存
   writer.Flush() //将缓存的内容写入到文件
}

将一个文件内容写入到另一个文件中(文件都已经存在)

func main()  {
    filepath1 :="c:/go/golang1.txt"
    filepath2 :="c:/go/golang2.txt"
    data,err := ioutil.ReadFile(filepath1) //读出一个文件的所有内容,只适合小文件内容的
    if err != nil{
        fmt.Println("读取文件失败 err=",err)
        return
    }
    err = ioutil.WriteFile(filepath2,data,0666)
    if err != nil{
        fmt.Println("写入错误 err=",err)
    }
}

 golang判断文件或文件夹是否存在的方法为:os.Stat() 函数返回的错误值进行判断:

  1)如果返回的错误为nil说明文件或文件夹存在

  2)如果返回的错误类型使用os.IsNoExist()判断为true 说明文件或文件夹不存在

  3)如果返回的错误为其他类型,则不确定是否存在

func PathExists(paths string)(bool,error)  {
    _,err := os.Stat(paths)
    if err == nil{
        return true,nil
    }
    if os.IsNotExist(err){//判断错误是不是不存在的错误
        return false,nil
    }
    return false,nil
}
func main()  {
    //filepath1 :="c:/go/golang1.txt"
    filepath2 :="c:/go/golang3.txt"
    bos,err :=PathExists(filepath2)
    fmt.Println(bos,err)
}

将一个图片/电影/视频 拷贝到另一个目录下

func Copy(dst Writer, src Reader) (written int64, err error)

package main
import (
    "bufio"
    "fmt"
    "io"
    "os"
)
func CopyFile(dstFileName string,srcFileName string)(writtern int64,err error)  {
  srcfile,err := os.Open(srcFileName)
  if err !=nil{
      fmt.Println("打开文件错误,err=",err)
  }
  defer srcfile.Close()
  reader := bufio.NewReader(srcfile)
  dstFile,err :=os.OpenFile(dstFileName,os.O_WRONLY|os.O_CREATE,0666)
  if err != nil{
      fmt.Println("打开文件失败err=",err)
  }
  defer dstFile.Close()
  writer :=bufio.NewWriter(dstFile)
  return io.Copy(writer,reader)
}

func main()  {
    srcFile :="1.jpg"
    dstFile :="./model/2.jpg"
    _,err :=CopyFile(dstFile,srcFile)
    if err != nil{
        fmt.Println("拷贝失败err=",err)
    }
}

统计英文,数字,空格,和其他字符的数量

说明:统计一个文件中含有的英文,数字,空格及其它的字符数量

//定义一个结构体用户保存统计结构
type CharCount struct {
    ChCount int //记录英文个数
    NumCount int //记录数字个数
    SpaceCount int //记录空格个数
    OtherCount int //记录其他字符个数
}
func main()  {
    filepath :="abc.txt"
    file,err :=os.Open(filepath)
    if err != nil{
        fmt.Println("打开文件错误")
        return
    }
    defer file.Close()
    var count CharCount //记录结果的结构体
    reader :=bufio.NewReader(file)
    for{
        str,err := reader.ReadString('\n')
        if err == io.EOF{
            break
        }
        //遍历str 进行统计
        for _,v := range str{
            switch {
            case v>'a' && v<'z':
                fallthrough //穿透
            case v>'A' && v<'Z':
                count.ChCount++
            case v ==  ' ' || v == '\t':
                count.SpaceCount++
            case v >='0' && v<=9:
                count.NumCount++
            default:
                count.OtherCount++

            }
        }

    }
    fmt.Printf("数字的字符个数:%v,字符的个数为:%v,空格的字符为:%v,其他字符的个数:%v",count.NumCount,count.ChCount,count.SpaceCount,count.OtherCount)
}

二十三.命令行参数

基本介绍

os.Args 是一个string的切片,用来存储所有的命令行参数

请编写一段代码,可以获取命令行各个参数

func main() {
    fmt.Println("命令行的参数有:",len(os.Args))
    for i,v:=range os.Args{
        fmt.Printf("args[%v]=%v\n",i,v)
    }
}

 flag包用来解析命令行参数

说明:前面的方式是比较原生的方式,对解析参数不是特别的方便,特别是带有指定参数形式的命令行。

比如:cmd>main.ext -f c:/aaa.txt -p 200 -u root 这样的形式命令行,go设计者给我们提供了 flag 包,可以方便的解析命令行参数,而且参数顺序可以随意

请编写一段代码获取各个输入参数

func main()  {
    //定义几个变量,用于接受命令行参数
    var user string
    var pwd string
    var host string
    var port int
    //&user 就是接受用户命令行中输入的 -u 后面的参数值
    //"u",就是 -u 指定参数
    //"",默认值
    //"用户名,默认为空" 说明
    flag.StringVar(&user,"u","","用户名,默认为空")
    flag.StringVar(&pwd,"p","","密码,默认为空")
    flag.StringVar(&host,"h","localhost","主机名,默认为localhost")
    flag.IntVar(&port,"port",3306,"端口号,默认为3306")
    //这里有一个非常终于的操作,转换,必须调用该方法
    flag.Parse()
    //输入结果
    fmt.Printf("user=%v pwd=%v host=%v port=%v",user,pwd,host,port)
}

二十四.json数据格式 和 tag的使用(指定标签)

结构体序列化

package main

import (
    "encoding/json"
    "fmt"
)

type Monster struct {
    Name     string  `json:"name"`   //首字母大写才能挎包使用,不然 下面的 json.Marshal 会丢失 Name
    Age      int     `json:"age"`
    Birthday string  `json:"birthday"`
    Sal      float64 `json:"sal"`
    Skill    string  `json:"skill"`
}
//结构体的序列化
func testStruct()  {
    monster := Monster{
        Name:     "牛魔王",
        Age:      500,
        Birthday: "2022-01-05",
        Sal:      8000.0,
        Skill:    "牛魔权",
    }
    data, err := json.Marshal(&monster) //返回一个byte切片
    if err != nil {
        fmt.Println("序列化错误 err=", err)
    }
    fmt.Printf("结构体序列化的结果:%v\n", string(data))
}
//切片的序列化
func testSlice()  {
    var slice []map[string]interface{}
    var m1 map[string]interface{}
    m1 = make(map[string]interface{})
    m1["name"] = "job"
    m1["age"] = 45
    m1["address"] = "深圳龙岗区"
    slice = append(slice,m1)
    m2 := make(map[string]interface{})
    m2["name"] = "top"
    m2["age"] = 5
    m2["address"] = [2]string{"深圳宝安","深圳福田"}
    slice = append(slice,m2)
    data, err := json.Marshal(&slice) //返回一个byte切片
    if err != nil {
        fmt.Println("序列化错误 err=", err)
    }
    fmt.Printf("切片序列化的结果:%v\n", string(data))
}
//基本类型的序列化
func testFloat64()  {
    var num1 float64 = 2354.54
    data, err := json.Marshal(&num1) //返回一个byte切片
    if err != nil {
        fmt.Println("序列化错误 err=", err)
    }
    fmt.Printf("基本类型序列化的结果:%v\n", string(data))
}
func main() {
    testStruct()
    testSlice()
    testFloat64()
}

结果:

结构体序列化的结果:{"name":"牛魔王","age":500,"birthday":"2022-01-05","sal":8000,"skill":"牛魔权"}
切片序列化的结果:[{"address":"深圳龙岗区","age":45,"name":"job"},{"address":["深圳宝安","深圳福田"],"age":5,"name":"top"}]
基本类型序列化的结果:2354.54

 将json字符串反序列化为结构体

package main

import (
    "encoding/json"
    "fmt"
)
type Monster struct {
    Name     string
    Age      int
    Birthday string
    Sal      float64
    Skill    string
}
//将json反序列化成结构体
func unmarshalStruct()  {
    str :=`{"name":"牛魔王","age":500,"birthday":"2022-01-05","sal":8000,"skill":"牛魔权"}`;
    //定义一个Monster实例
    var monster Monster
    err :=json.Unmarshal([]byte(str),&monster)
    if err != nil{
        fmt.Printf("Unmarshal err=%v\n",err)
    }
    fmt.Printf("反序列化后 monster=%v\n monster.Name=%v\n",monster,monster.Name)
}
//将json字符串反序列化为map
func unmarshalMap()  {
    str :=`{"address":"深圳龙岗区","age":45,"name":"job"}`
    //定义一个map
    var a map[string]interface{}
    //反序列化
    //注意:反序列化map 不需要make,因为make操作被封装到了 Unmarshal函数
    err :=json.Unmarshal([]byte(str),&a)
    if err != nil{
        fmt.Printf("Unmarshal err=%v\n",err)
    }
    fmt.Printf("反序列化后map=%v\n",a)
}
//将json字符串反序列化为slice
func unmarshalSlice()  {
    str :=`[{"address":"深圳龙岗区","age":45,"name":"job"},{"address":["深圳宝安","深圳福田"],"age":5,"name":"top"}]`
    var b []map[string]interface{}
    //注意:反序列化map 不需要make,因为make操作被封装到了 Unmarshal函数
    err :=json.Unmarshal([]byte(str),&b)
    if err != nil{
        fmt.Printf("Unmarshal err=%v\n",err)
    }
    fmt.Printf("反序列化后的切片=%v\n",b)

}
func main()  {
   unmarshalStruct()
   unmarshalMap()
   unmarshalSlice()
}

结果:

1)在反序列化一个json字符串时,要确保反序列化后的数据类型和原来的序列化前的数据类型一致
反序列化后 monster={牛魔王 500 2022-01-05 8000 牛魔权}
 monster.Name=牛魔王
反序列化后map=map[address:深圳龙岗区 age:45 name:job]
反序列化后的切片=[map[address:深圳龙岗区 age:45 name:job] map[address:[深圳宝安 深圳福田] age:5 name:top]]

二十四.单元测试

GO语言中自带有一个轻量级的测试框架testing和自带的go test 命令来实现单元测试和性能测试,testing框架和其他语言中的测试框架类似,可以基于这个框架写针对相应的测试用例,也可以基于该框架写相应的压力测试用例.通过单元测试,可以解决如下问题:

1)确保每个函数是可运行,并且运行结果是正确的

2)确保写处理的代码性能是好的

3)单元测试能及时发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定

 packago/myAccount/cal.go

package main

func addUpper(n int)(int) {
var sum = 0
for i:=1;i<=n;i++{
sum +=i
}
return sum
}

func getsub(x,y int)int {
return x-y
}

packago/myAccount/cal_test.go

package cal

import (
"testing"
)
func TestAddUpper(t *testing.T) {
res :=addUpper(10)
if res != 55{
t.Fatalf("AddUpper(10) 执行错误,期望值=%v 实际值=%v\n",55,res)
}
t.Logf("AddUpper(10) 执行正确,期望值=%v 实际值=%v\n",55,res)
}

packago/myAccount/sub_test.go

package cal

import "testing"

func TestSub(t *testing.T)  {
    res :=getsub(10,5)
    if res != 5{
        t.Fatalf("getsub(10,5) 执行错误,期望值=%v 实际值=%v\n",5,res)
    }
    t.Logf("getsub(10,5) 执行正确,期望值=%v 实际值=%v\n",5,res)
}

测试结果:go  test  -v     会扫描所有的_test.go 测试用例执行

=== RUN TestAddUpper
cal_test.go:11: AddUpper(10) 执行正确,期望值=55 实际值=55
--- PASS: TestAddUpper (0.00s)
=== RUN TestSub
sub_test.go:10: getsub(10,5) 执行正确,期望值=5 实际值=5
--- PASS: TestSub (0.00s)
PASS
ok packago/myAccount 0.402s

1) 测试用例文件名必须以 _test.go 结尾。比如 cal_test.go , cal 不是固定的

2) 测试用例函数必须以 Test开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper.

官方说明:func TestXxx(*testing.T) 其中 Xxx 可以是任何字母数字字符串(但第一个字母不能是 [a-z]小写的),用于识别测试例程。

3) TestAddUpper(T *testing.T)的形参类型必须是 *testing.T

4) 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper,TestSub

5) 运行测试用例指令

(1) cmd>go test[如果运行正确,无日志,错误时,会输出日志]

(2) cmd>go test -v [运行正确或错误,都会输出日志]

6) 当出现错误时,可以使用t.Fatalf来格式化输出错误信息,并退出程序

7) t.Logf方法可以输出相应的日志

8) 测试用例函数,并没有放到main函数中,也执行了,这就是测试用例的方便之处

9) PASS 表示测试用例运行成功,FALL表示测试用例运行失败

10) 测试单个文件,一定要带上被测试的源文件 go test -v cal_test.go cal.go

C:\goprojects\src\packago\myAccount>go test -v sub_test.go cal.go
=== RUN TestSub
sub_test.go:10: getsub(10,5) 执行正确,期望值=5 实际值=5
--- PASS: TestSub (0.00s)
PASS
ok command-line-arguments (cached)

11) 测试单个方法 go test -v -test.run TestAddUpper

=== RUN TestAddUpper
cal_test.go:11: AddUpper(10) 执行正确,期望值=55 实际值=55
--- PASS: TestAddUpper (0.00s)
PASS
ok packago/myAccount 0.409s

1) 编写一个Monster结构体,字段Name,Age,Skill

2) 给Monster绑定方法Store,可以将一个Monster变量,序列化后保存到文件中

3) 给Monster绑定方法ReStore,可以将一个序列化的Monster,从文件中读取,并反序列化为Monster对象

4) 编程测试用例文件 store_test.go,编写测试用例函数TestStore和TestRestore进行测试

monster.go

package model

import (
"encoding/json"
"fmt"
"io/ioutil"
)

type Monster struct {
Name string
Age int
Skill string
}

func (this *Monster)Store() bool {
data,err := json.Marshal(this)
fmt.Println(data)
if err !=nil{
fmt.Println("序列化错误 err=", err)
return false
}
filepath :="c:/go/golang.txt"
err = ioutil.WriteFile(filepath,data,0666)
if err != nil{
fmt.Println("打开文件错误,err=",err)
return false
}
return true
}
func (this *Monster)ReStore() bool {
filepath :="c:/go/golang.txt"
data,err :=ioutil.ReadFile(filepath)
if err !=nil{
fmt.Println("读取文件错误 err=", err)
return false
}
err = json.Unmarshal(data,this)
if err !=nil{
fmt.Println("反序列化失败 err=", err)
return false
}
return true
}

monster_test.go

package model

import "testing"

func TestStore(t *testing.T) {
    monster := Monster{
        Name:  "红孩儿",
        Age:   900,
        Skill: "三头六臂",
    }
    res :=monster.Store()
    if !res{
        t.Fatalf("monster.Store() 错误,希望为:%v 实际为%v",true,res)
    }
    t.Logf("monster.Store() 测试成功")
}

func TestReStore(t *testing.T) {
    var monster Monster
    res :=monster.ReStore()
    if !res{
        t.Fatalf("monster.ReStore() 错误,希望为:%v 实际为%v",true,res)
    }
    if monster.Name != "红孩儿"{
        t.Fatalf("monster.ReStore() 错误,希望为:%v 实际为%v",true,res)
    }
    t.Logf("monster.ReStore() 测试成功")
}

二十五.Goroutine(协程)

25.1基本介绍

进程和线程

1) 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位

2) 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位

3) 一个进程可以创建核销多个线程,同一个进程中的多个线程可以并发执行

4) 一个程序至少有一个进程,一个进程至少有一个线程

并发和并行

1) 多线程程序在一个核的cpu上运行,就是并发。

解释:因为是在一个CPU上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,

在某一个时间点看,其实只有一个线程在执行,这就是并发。

 

2) 多线程程序在多个核的cpu上运行,就是并行。

解释:因为是多个CPU上(比如有10个CPU),比如有10个线程,每个线程执行10毫秒(各自在不同CPU上执行),从人的角度看,这10个线程都在运行,

但从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行

协程和线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

线程:一个线程上可以跑多个协程,协程是轻量级的线程。

 25.2 go协程和go的主线程

1) go主线程(有程序员直接称为线程/也可以理解成进程):一个GO线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程。

2) go协程的特点 1.有独立的栈空间 2.共享程序堆空间 3.调度由用户控制 4.协程是轻量级的线程

小知识:

堆和栈的区别主要有五大点,分别是:

1、申请方式的不同。栈由系统自动分配,而堆是人为申请开辟;

2、申请大小的不同。栈获得的空间较小,而堆获得的空间较大;

3、申请效率的不同。栈由系统自动分配,速度较快,而堆一般速度比较慢;

4、存储内容的不同。栈在函数调用时,函数调用语句的下一条可执行语句的地址第一个进栈,

然后函数的各个参数进栈,其中静态变量是不入栈的。而堆一般是在头部用一个字节存放堆的大小,堆中的具体内容是人为安排;

5、底层不同。栈是连续的空间,而堆是不连续的空间。

package main
import (
    "fmt"
    "strconv"
    "time"
)
func test(){
    for i:=0;i<=10;i++{
        fmt.Println("test() hello,world "+strconv.Itoa(i))
        time.Sleep(time.Second)
    }
}
func main() {
    go test()//开启一个协程
    for i:=0;i<=10;i++{
        fmt.Println("main() hello,golang "+strconv.Itoa(i))
        time.Sleep(time.Second)
    }
}

结果:main主线程和test协程在同时执行

main() hello,golang 0
test() hello,world 0
main() hello,golang 1
test() hello,world 1
test() hello,world 2
main() hello,golang 2
test() hello,world 3
main() hello,golang 3
...

示意图:

1) 主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源。

2) 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。

3) golang的协程机制是重要的特点,可以轻松的开启上万个协程。其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就是突显golang在并发上的优势了

25.3 MPG基本模式介绍

 

25.4 设置golang使用CPU的个数

package main
import (
    "fmt"
    "runtime"
)
func main() {
    //获取当前系统CPU的数量
    num:=runtime.NumCPU()
    //设置使用多少个CPU
    runtime.GOMAXPROCS(num-1)//留一个CPU给其他程序使用
    fmt.Println(num)
}

1)  go1.8后,默认让程序运行在多个核上,可以不用设置了

2)  go1.8前,还是要设置一下,可以更高效的利用CPU

Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行

这个函数的作用是让当前 goroutine 让出 CPU,当一个 goroutine 发生阻塞,Go 会自动地把与该 goroutine 处于同一系统线程的其他 goroutine 转移到另一个系统线程上去,以使这些 goroutine 不阻塞。

func main() {
    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("goroutine。。。")
        }
    }()
    for i := 0; i < 4; i++ {
        //让出时间片,先让别的协议执行,它执行完,再回来执行此协程
        runtime.Gosched()
        fmt.Println("main。。")
    }
}

Goexit:退出当前 goroutine(但是defer语句会照常执行)

func main() {
    //创建新建的协程
    go func() {
        fmt.Println("goroutine开始。。。")
        //调用了别的函数
        fun()
        fmt.Println("goroutine结束。。")
    }() //别忘了()
    //睡一会儿,不让主协程结束
    time.Sleep(3*time.Second)
}

func fun() {
    defer fmt.Println("defer。。。")
    //return           //终止此函数
    runtime.Goexit() //终止所在的协程
    fmt.Println("fun函数。。。")
}

NumGoroutine:返回正在执行和排队的任务总数

runtime.NumGoroutine函数在被调用后,会返回系统中的处于特定状态的Goroutine的数量。这里的特指是指Grunnable\Gruning\Gsyscall\Gwaition。处于这些状态的Groutine即被看做是活跃的或者说正在被调度。

注意:垃圾回收所在Groutine的状态也处于这个范围内的话,也会被纳入该计数器。

GOOS:目标操作系统

runtime.GC:会让运行时系统进行一次强制性的垃圾收集

强制的垃圾回收:不管怎样,都要进行的垃圾回收。
非强制的垃圾回收:只会在一定条件下进行的垃圾回收(即运行时,系统自上次垃圾回收之后新申请的堆内存的单元(也成为单元增量)达到指定的数值)。
GOROOT :获取goroot目录

GOOS : 查看目标操作系统 很多时候,我们会根据平台的不同实现不同的操作,就而已用GOOS了:

//获取goroot目录:
fmt.Println("GOROOT-->",runtime.GOROOT())
//获取操作系统
fmt.Println("os/platform-->",runtime.GOOS) // GOOS--> darwin,mac系统

临界资源安全问题

临界资源: 指并发环境中多个进程/线程/协程共享的资源。

但是在并发编程中对临界资源的处理不当, 往往会导致数据不一致的问题。

示例代码:

package main
import (
    "fmt"
    "time"
)
func main()  {
    a := 1
    go func() {
        a = 2
        fmt.Println("子goroutine。。",a)
    }()
    a = 3
    time.Sleep(1)
    fmt.Println("main goroutine。。",a)
}

结果:

 能够发现一处被多个goroutine共享的数据。

并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。

如果多个goroutine在访问同一个数据资源的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine来讲,这个数值可能是不对的。

举个例子,我们通过并发来实现火车站售票这个程序。一共有100张票,4个售票口同时出售。

我们先来看一下示例代码:

package main
import (
    "fmt"
    "math/rand"
    "time"
)
//全局变量
var ticket = 10 // 100张票
func main() {
    /*
        4个goroutine,模拟4个售票口,4个子程序操作同一个共享数据。
    */
    go saleTickets("售票口1") // g1,100
    go saleTickets("售票口2") // g2,100
    go saleTickets("售票口3") //g3,100
    go saleTickets("售票口4") //g4,100
    time.Sleep(5*time.Second)
}
func saleTickets(name string) {
    rand.Seed(time.Now().UnixNano())
    for { //ticket=1
        if ticket > 0 { //g1,g3,g2,g4
            //睡眠
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            // g1 ,g3, g2,g4
            fmt.Println(name, "售出:", ticket)  // 1 , 0, -1 , -2
            ticket--   //0 , -1 ,-2 , -3
        } else {
            fmt.Println(name,"售罄,没有票了。。")
            break
        }
    }
}

结果:

 

 

 我们为了更好的观察临界资源问题,每个goroutine先睡眠一个随机数,然后再售票,我们发现程序的运行结果,还可以卖出编号为负数的票。

分析:

我们的卖票逻辑是先判断票数的编号是否为负数,如果大于0,然后我们就进行卖票,只不过在卖票钱先睡眠,然后再卖,假如说此时已经卖票到只剩最后1张了,某一个goroutine持有了CPU的时间片,那么它再片段是否有票的时候,条件是成立的,所以它可以卖票编号为1的最后一张票。但是因为它在卖之前,先睡眠了,那么其他的goroutine就会持有CPU的时间片,而此时这张票还没有被卖出,那么第二个goroutine再判断是否有票的时候,条件也是成立的,那么它可以卖出这张票,然而它也进入了睡眠。。其他的第三个第四个goroutine都是这样的逻辑,当某个goroutine醒来的时候,不会再判断是否有票,而是直接售出,这样就卖出最后一张票了,然而其他的goroutine醒来的时候,就会陆续卖出了第0张,-1张,-2张。

这就是临界资源的不安全问题。某一个goroutine在访问某个数据资源的时候,按照数值,已经判断好了条件,然后又被其他的goroutine抢占了资源,并修改了数值,等这个goroutine再继续访问这个数据的时候,数值已经不对了。

临界资源安全问题的解决

临界资源安全问题的解决要想解决临界资源安全的问题,很多编程语言的解决方案都是同步。通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个共享数据,当前goroutine访问完毕,解锁后,其他的goroutine才能来访问。

我们可以借助于sync包下的锁操作。

示例代码:

package main
import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)
//全局变量
var ticket = 10 // 100张票
var wg sync.WaitGroup
var matex sync.Mutex // 创建锁头
func main() {
    /*
        4个goroutine,模拟4个售票口,4个子程序操作同一个共享数据。
    */
    wg.Add(4)
    go saleTickets("售票口1") // g1,100
    go saleTickets("售票口2") // g2,100
    go saleTickets("售票口3") //g3,100
    go saleTickets("售票口4") //g4,100
    wg.Wait()              // main要等待。。。

    //time.Sleep(5*time.Second)
}

func saleTickets(name string) {
    rand.Seed(time.Now().UnixNano())
    defer wg.Done()
    for { //ticket=1
        matex.Lock()
        if ticket > 0 { //g1,g3,g2,g4
            //睡眠
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            // g1 ,g3, g2,g4
            fmt.Println(name, "售出:", ticket) // 1 , 0, -1 , -2
            ticket--                         //0 , -1 ,-2 , -3
        } else {
            matex.Unlock() //解锁
            fmt.Println(name, "售罄,没有票了。。")
            break
        }
        matex.Unlock() //解锁
    }
}

在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信,而要以通信的方式去共享内存。

在Go语言中并不鼓励用锁保护共享状态的方式在不同的Goroutine中分享信息(以共享内存的方式去通信)。而是鼓励通过channel将共享状态或共享状态的变化在各个Goroutine之间传递(以通信的方式去共享内存),这样同样能像用锁一样保证在同一的时间只有一个Goroutine访问共享状态。

当然,在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多。

sync包

sync是synchronization同步这个词的缩写,所以也会叫做同步包。这里提供了基本同步的操作,比如互斥锁等等。这里除了Once和WaitGroup类型之外,大多数类型都是供低级库例程使用的。更高级别的同步最好通过channel通道和communication通信来完成

WaitGroup

WaitGroup,同步等待组。

 

在类型上,它是一个结构体。一个WaitGroup的用途是等待一个goroutine的集合执行完成。主goroutine调用了Add()方法来设置要等待的goroutine的数量。然后,每个goroutine都会执行并且执行完成后调用Done()这个方法。与此同时,可以使用Wait()方法来阻塞,直到所有的goroutine都执行完成。

Add()方法:

Add这个方法,用来设置到WaitGroup的计数器的值。我们可以理解为每个waitgroup中都有一个计数器 用来表示这个同步等待组中要执行的goroutin的数量。

 

如果计数器的数值变为0,那么就表示等待时被阻塞的goroutine都被释放,如果计数器的数值为负数,那么就会引发恐慌,程序就报错了。

Done()方法

 

Done()方法,就是当WaitGroup同步等待组中的某个goroutine执行完毕后,设置这个WaitGroup的counter数值减1。

Wait()方法

Wait()方法,表示让当前的goroutine等待,进入阻塞状态。一直到WaitGroup的计数器为零。才能解除阻塞, 这个goroutine才能继续执行。

我们创建并启动两个goroutine,来打印数字和字母,并在main goroutine中,将这两个子goroutine加入到一个WaitGroup中,同时让main goroutine进入Wait(),让两个子goroutine先执行。当每个子goroutine执行完毕后,调用Done()方法,设置WaitGroup的counter减1。当两条子goroutine都执行完毕后,WaitGroup中的counter的数值为零,解除main goroutine的阻塞。

 

示例代码:

 

package main
import (
    "fmt"
    "sync"
)
var wg sync.WaitGroup // 创建同步等待组对象
func main()  {
    /*
        WaitGroup:同步等待组
            可以使用Add(),设置等待组中要 执行的子goroutine的数量,

            在main 函数中,使用wait(),让主程序处于等待状态。直到等待组中子程序执行完毕。解除阻塞

            子gorotuine对应的函数中。wg.Done(),用于让等待组中的子程序的数量减1
    */
    //设置等待组中,要执行的goroutine的数量
    wg.Add(2)
    go fun1()
    go fun2()
    fmt.Println("main进入阻塞状态。。。等待wg中的子goroutine结束。。")
    wg.Wait() //表示main goroutine进入等待,意味着阻塞
    fmt.Println("main,解除阻塞。。")

}
func fun1()  {
    for i:=1;i<=10;i++{
        fmt.Println("fun1.。。i:",i)
    }
    wg.Done() //给wg等待中的执行的goroutine数量减1.同Add(-1)
}
func fun2()  {
    defer wg.Done()
    for j:=1;j<=10;j++{
        fmt.Println("\tfun2..j,",j)
    }
}

 

结果:

 

 

 Mutex(互斥锁)

通过上一小节,我们知道了在并发程序中,会存在临界资源问题。就是当多个协程来访问共享的数据资源,那么这个共享资源是不安全的。为了解决协程同步的问题我们使用了channel,但是Go语言也提供了传统的同步工具

什么是锁呢?就是某个协程(线程)在访问某个资源时先锁住,防止其它协程的访问,等访问完毕解锁后其他协程再来加锁进行访问。一般用于处理并发中的临界资源问题。

Go语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex。

Mutex 是最简单的一种锁类型,互斥锁,同时也比较暴力,当一个 goroutine 获得了 Mutex 后,其他 goroutine 就只能乖乖等到这个 goroutine 释放该 Mutex。

每个资源都对应于一个可称为 “互斥锁” 的标记,这个标记用来保证在任意时刻,只能有一个协程(线程)访问该资源。其它的协程只能等待。

互斥锁是传统并发编程对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法,Lock和Unlock。Lock锁定当前的共享资源,Unlock进行解锁。

 

在使用互斥锁时,一定要注意:对资源操作完成后,一定要解锁,否则会出现流程执行异常,死锁等问题。通常借助defer。锁定后,立即使用defer语句保证互斥锁及时解锁。

Lock()方法:

Lock()这个方法,锁定m。如果该锁已在使用中,则调用goroutine将阻塞,直到互斥体可用。

Unlock()方法

Unlock()方法,解锁解锁m。如果m未在要解锁的条目上锁定,则为运行时错误。

锁定的互斥体不与特定的goroutine关联。允许一个goroutine锁定互斥体,然后安排另一个goroutine解锁互斥体。

 

我们针对于上次课程汇总,使用goroutine,模拟4个售票口出售火车票的案例。4个售票口同时卖票,会发生临界资源数据安全问题。我们使用互斥锁解决一下。(Go语言推崇的是使用Channel来实现数据共享,但是也还是提供了传统的同步处理方式)

package main

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

//全局变量,表示票
var ticket = 10 //100张票


var mutex sync.Mutex //创建锁头

var wg sync.WaitGroup //同步等待组对象
func main() {
    /*
    4个goroutine,模拟4个售票口,


    在使用互斥锁的时候,对资源操作完,一定要解锁。否则会出现程序异常,死锁等问题。
    defer语句
     */

     wg.Add(4)
    go saleTickets("售票口1")
    go saleTickets("售票口2")
    go saleTickets("售票口3")
    go saleTickets("售票口4")

    wg.Wait() //main要等待
    fmt.Println("程序结束了。。。")

    //time.Sleep(5*time.Second)
}

func saleTickets(name string){
    rand.Seed(time.Now().UnixNano())
    defer wg.Done()
    for{
        //上锁
        mutex.Lock() //g2
        if ticket > 0{ //ticket 1 g1
            time.Sleep(time.Duration(rand.Intn(1000))*time.Millisecond)
            fmt.Println(name,"售出:",ticket) // 1
            ticket-- // 0
        }else{
            mutex.Unlock() //条件不满足,也要解锁
            fmt.Println(name,"售罄,没有票了。。")
            break
        }
        mutex.Unlock() //解锁
    }
}

  RWMutex(读写锁)

通过对互斥锁的学习,我们已经知道了锁的概念以及用途。主要是用于处理并发中的临界资源问题。

Go语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex。其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能。

RWMutex是读/写互斥锁。锁可以由任意数量的读取器或单个编写器持有。RWMutex的零值是未锁定的mutex。

如果一个goroutine持有一个rRWMutex进行读取,而另一个goroutine可能调用lock,那么在释放初始读取锁之前,任何goroutine都不应该期望能够获取读取锁。特别是,这禁止递归读取锁定。这是为了确保锁最终可用;被阻止的锁调用会将新的读卡器排除在获取锁之外。

我们怎么理解读写锁呢?当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,你给我站一边去,等它们读(读解锁)完你再来写(写锁定)。我们可以将其总结为如下三条:

同时只能有一个 goroutine 能够获得写锁定。
同时可以有任意多个 gorouinte 获得读锁定。
同时只能存在写锁定或读锁定(读和写互斥)。
所以,RWMutex这个读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景。

读写锁的写锁只能锁定一次,解锁前不能多次锁定,读锁可以多次,但读解锁次数最多只能比读锁次数多一次,一般情况下我们不建议读解锁次数多余读锁次数。

基本遵循两大原则:

1、可以随便读,多个goroutine同时读。

2、写的时候,啥也不能干。不能读也不能写。

读写锁即是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。读写锁遵循的访问控制规则与互斥锁有所不同。在读写锁管辖的范围内,它允许任意个读操作的同时进行。但是在同一时刻,它只允许有一个写操作在进行。

并且在某一个写操作被进行的过程中,读操作的进行也是不被允许的。也就是说读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间却不存在互斥关系。

二十六.channel(管道)

需求:现在要计算 1-200 的各个数的阶乘,并把各个数的阶乘放入到map中。最后显示出来。要求用goroutine完成

分析思路:

1) 使用goroutine来完成,效率高,但是会出现并发/并行安全问题

2) 这里就提出了不同goroutine如何通信的问题

代码实现

1) 使用 goroutine 来完成 (看看使用 goroutine 并发完成会出现什么问题?然后我们如何去解决)

2) 在运行某个程序时,如何指定是否存在资源竞争问题。在编译该程序时,增加一个参数 -race 即可

package main
import (
    "fmt"
    "time"
)
var myMap = make(map[int]int,10)
func test(n int)  {
    res :=1
    for i:=1;i<=n;i++ {
        res *=i
    }
    myMap[n] = res;
}
func main() {
    for i:=1; i<=200;i++  {
       go test(i)//开启200个协程
    }
    time.Sleep(time.Second * 10)
    for i,v :=range myMap{
        fmt.Printf("map[%d]=%d\n",i,v)
    }
}

结果:fatal error: concurrent map writes

同时两百个协程同时操作了mymap 资源竞争

不同goroutine 之间如何通讯

1)  全局变量加锁同步

2)  channel

使用全局变量加锁同步改进程序

1) 因为没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示concurrent map writes

2) 解决方案:加入互斥锁

3) 我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += uint64(i)

package main
import (
    "fmt"
    "sync"
    "time"
)
var myMap = make(map[int]float64,10)
var lock sync.Mutex //互斥锁
func test(n int)  {
    var res float64 = 1
    for i:=1;i<=n;i++ {
        res *=float64(i)
    }
    lock.Lock()//加锁
    myMap[n] = res;
    lock.Unlock()//解锁
}
func main() {
    for i:=1; i<=200;i++  {
       go test(i)
    }
    time.Sleep(time.Second * 2)
    lock.Lock()
    for i,v:=range myMap{
        fmt.Printf("map[%d]=%f\n",i,v)
    }
    lock.Unlock()
}
/*读为什么需要加互斥锁,按理说10秒数上面的协程都应该执行完,后面就不应该出现资源竞争的问题了,但是在实际运行中,还是可能
出现资源竞争,因为我们程序从设计上可以知道10秒就执行完所有协程,但是主线程并不知道,因此底层可能仍然出现资源真多,因此
加入互斥锁即可解决问题。*/

结果:

map[10]=3628800.000000
map[35]=10333147966386144222209170348167175077888.000000
map[34]=295232799039604119555149671006000381952.000000
map[49]=608281864034267522488601608116731623168777542102418391010639872.000000
map[80]=71569457046263778832073404098641551692451427821500630228331524401978643519022131505852398484420816675798776564959674368.000000
map[124]=150614174151114036108297093562510697.....
map[6]=720.000000

为什么需要channel

1) 前面使用了全局变量加锁同步来解决goroutine的通讯,但不完美

2) 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置了10秒,仅仅是估算

3) 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态,这时也会随主线程的退出而销毁

4) 通过全局变量加锁同步来实现通讯,也并不利于多个协程对全部变量的读写操作。

5) 上面种种分析都在呼唤一个新的通讯机制 - channel

channel 的基本介绍

1) channel 本质就是一个数据结构-队列

2) 数据是先进先出【FIFO:first in first out】

3) 线程安全,多 goroutine 访问时,不需要加锁,就是说channel 本身就是线程安全的

4) channel 有类型的,一个 string 的 channel 只能存放 string 类型的数据

 

 

var 变量名 chan 数据类型

举例:

var  intChan      chan int (intChan 用于存放 int 数据)

var  mapChan  chan map[int]string (mapChan 用于存放 map[int]string类型)

var  perChan   chan Person

var  perChan2 chan *Person

说明:

channel 是引用类型

channel 必须初始化才能写入数据,即 make后才能使用

package main
import "fmt"
func main() {
var intChan chan int
intChan = make(chan int,3)
fmt.Printf("intChan的值 %v, intChan本身的地址 %p\n",intChan,&intChan)
//向管道写入数据,写入的数据不能超过管道的容量
intChan<- 10
num :=20
intChan<- num
intChan<- 50
fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan))
//向管道中读取数据,在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock
num2 := <-intChan
fmt.Printf("num2=%v\n",num2)
fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan))
num3 :=<-intChan
num4 :=<-intChan
fmt.Println("num3=",num3,"num4=",num4)
}

结果:

intChan的值 0xc000104000, intChan本身的地址 0xc000006028
channel len=3 cap=3 
num2=10
channel len=2 cap=3 
num3= 20 num4= 50

channel(管道)-使用注意事项

1.channel中只能存放指定的数据类型

2.channel的数据放满后,就不能再放入了

3.如果从channel取出数据后,可以继续放入

4.在没有使用协程的情况下,如果channel数据取完了,再取,就会报 dead lock

创建一个mapChan,最多可以存放10个map[string]string的key-val

package main
import "fmt"
func main() {
    var mapChan chan map[string]string
    mapChan = make(chan map[string]string,10)
    m1 :=make(map[string]string,20)
    m1["city1"] = "北京"
    m1["city2"] = "天津"

    m2 :=make(map[string]string,20)
    m2["hero1"] = "宋江"
    m2["hero2"] = "武松"
    mapChan<-m1
    mapChan<-m2
    fmt.Printf("mapChan len=%v , cap=%v",len(mapChan),cap(mapChan)) //mapChan len=2 , cap=10
}

创建一个catChan,最多可以存放10个cat结构体变量

package main
import "fmt"
type cat struct {
    Name string
    Age int
}
func main() {
    var catChan chan cat
    catChan = make(chan cat,10)
    cat1 := cat{"tome~",18}
    cat2 := cat{"many",30}
    //放入到管道
    catChan<- cat1
    catChan<- cat2
    //从管道取出
    qcat1 :=<-catChan
    qcat2 :=<-catChan
    fmt.Println(qcat1,qcat2)
}

创建一个catChan2,最多可以存放10个*cat结构体变量

package main
import "fmt"
type cat struct {
    Name string
    Age int
}
func main() {
    var catChan2 chan *cat
    catChan2 = make(chan *cat,10)
    cat1 := cat{"tome~",18}
    cat2 := cat{"many",30}
    //放入到管道
    catChan2<- &cat1
    catChan2<- &cat2
    //从管道取出
    qcat1 :=<-catChan2
    qcat2 :=<-catChan2
    fmt.Println(qcat1,qcat2)
}

创建一个allChan,最多可以存放10个 任意数据类型变量

package main
import "fmt"
type cat struct {
    Name string
    Age int
}
func main() {
    var allChan chan interface{}
    allChan = make(chan interface{},10)
    cat1 := cat{"tom",20}
    cat2 := cat{"tix",30}
    allChan<- cat1
    allChan<- cat2
    allChan<- "jack"
    allChan<- 30
    qcat1 :=<-allChan
    qcat2 :=<-allChan
    v1:=<-allChan
    v2:=<-allChan
    fmt.Println(qcat1,qcat2,v1,v2) //{tom 20} {tix 30} jack 30
}

 空接口类型的数据必须使用类型断言

package main
import "fmt"
type Cat struct {
    Name string
    Age int
}
func main() {
    allChan := make(chan interface{},10)
    allChan<- 10
    allChan<- "tome jack"
    cat :=Cat{"小花猫",4}
    allChan<-cat
    //取第三个,丢弃前两个
    <-allChan
    <-allChan
    vcat := <-allChan
    fmt.Printf("vcat=%T,vcat=%v\n",vcat,vcat) //vcat=main.Cat,vcat={小花猫 4}
    //fmt.Printf("vcat.name=%v",vcat.Name) 错误的 空接口类型必须使用类型断言
    a:=vcat.(Cat)
    fmt.Printf("vcat.name=%v",a.Name)//vcat.name=小花猫

}

channel 的关闭

使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读数据

channel 的遍历

channel支持for-range的方式进行遍历,请注意两个细节

1) 在遍历时,如果channel没有关闭,则回出现deadlock的错误

package main
import "fmt"
func main() {
    allChan := make(chan int,3)
    allChan<- 10
    allChan<-20
    close(allChan)
    //allChan<-30 //报错,不能向关闭的通道写入数据
    n1 :=<-allChan //读取关闭的通道数据是没问题的
    fmt.Println(n1)
}

2) 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历

package main
import "fmt"
func main() {
intChan := make(chan int,100)
for i:=1;i<=100;i++{
intChan<- i*2 //放入100个数据
}
//遍历管道不能使用普通的 for 循环
//for i:=0;i<=len(intChan);i++{

//}
close(intChan) //不关闭管道会报 deadlock 错误
for v:=range intChan{
fmt.Println(v)
}
fmt.Printf("intChan len=%v,cap=%v",len(intChan),cap(intChan))//intChan len=0,cap=100 遍历也就取出了管道里面的数据
}

1.开启一个writeData协程,向管道intChan中写入50个整数

2.开启一个readData协程,从管道intChan中读取writeData写入的数据

3.注意:writeData和readData操作的是同一个管道

4.主线程需要等待writeData和readData协程都完成工作才能退出

package main
import (
    "fmt"
    "time"
)
//writeData
func writeData(intChan chan int)  {
    for i:=1;i<=50;i++{
        intChan<-i
        fmt.Println("writeData 写入数据",i)
        time.Sleep(time.Second)
    }
    close(intChan) //x, ok := <-c 还会将ok置为false。
}
//readData
func readData(intChan chan int,exitChan chan bool)  {
    for{
        v,ok:=<-intChan //会阻塞
        if !ok{ //intChan 管道被close 才会退出
            break
        }
        fmt.Printf("readData 读到数据=%v\n",v)
    }
    //readDate 读取完数据后,即任务完成
    exitChan<-true
    close(exitChan)
}
func main() {
    intChan :=make(chan int,50)
    exitChan :=make(chan bool,1)
    go writeData(intChan)
    go readData(intChan,exitChan)
    for{
        _,ok :=<-exitChan
        if !ok{
          break
        }
    }
    fmt.Println("main 线程结束")
}

channel 阻塞

1.协程写入管道,超过了容量,如果发现有协程在读,会阻塞,如果没有发现协程在读,会死锁deadlock,读写频率不一致没关系。

要求统计1-80000的数字中,那些是素数?

分析:

1.传统方法,就是使用一个循环,循环的判断各个数是不是素数

2.使用并发/并行的方式,将统计的素数任务分配给多个goroutine去完成,完成任务时间短

package main
import (
    "fmt"
    "time"
)
//向intChan放入1-8000个数
func putNum(intChan chan int)  {
    for i:=1;i<=8000;i++{
        intChan<-i
    }
    close(intChan)
}
//放入素数
func prineNum(intChan chan int,prineChan chan int,exitChan chan bool)  {
    var flag bool
    for{
        //time.Sleep(time.Millisecond*10)
        num,ok:= <-intChan
        if !ok{//取到了结束标识符
            break
        }
        flag = true //假设是素数
        for i:=2;i<num;i++{
            if num % i == 0{//说明不是素数
                flag = false
            }
        }
        if flag{
            prineChan <- num
        }
    }
    fmt.Println("有一个prineNum结束")
    //自己结束,写入结束标志
    exitChan<- true
}
func main()  {
    intChan :=make(chan int,1000)
    prineChan :=make(chan int,2000)//放入结果
    exitChan :=make(chan bool,4)//协程完成的标识
    start :=time.Now().Unix()
    go putNum(intChan)
    for i:=1;i<=4;i++ {
        go prineNum(intChan,prineChan,exitChan)
    }
    go func() {
        for i:=1;i<=4;i++{
            <-exitChan //取不到会阻塞
        }
        end :=time.Now().Unix()
        fmt.Println("使用协程所用的时间",end-start)
        //当我们从 exitChan取出了4个值 就可以关闭prineChan了
        close(prineChan)
    }()
    for{
        res,ok := <-prineChan
        if !ok{
            break
        }
        fmt.Printf("素数为=%d\n",res)
    }
    /*for v:=range prineChan{
        fmt.Printf("素数为=%d\n",v)
    }*/
    fmt.Println("主线程退出")
}

 1) channel 可以声明为只读,或者只写性质 (默认情况下,管道为双向管道,可读也可写)

package main
import "fmt"
func main()  {
    //1.声明为只写
    var chan1 chan<- int
    chan1 = make(chan int,3)
    chan1<- 20 //
    fmt.Println("chan1 = ",chan1)
    //2.声明为只读
    var chan2 <-chan int
    chan2 = make(chan int,3)
    num2 :=<-chan2 //
    fmt.Println("num2 = ",num2)
}

如果把双向通道传值给形参(只读通道),相当于把双向通道的读,封闭了

time包中的通道相关函数

主要就是定时器,标准库中的Timer让用户可以定义自己的超时逻辑,尤其是在应对select处理多个channel的超时、单channel读写的超时等情形时尤为方便。

Timer是一次性的时间触发事件,这点与Ticker不同,Ticker是按一定时间间隔持续触发时间事件。

 

Timer常见的创建方式:

t:= time.NewTimer(d)
t:= time.AfterFunc(d, f)
c:= time.After(d)

虽然说创建方式不同,但是原理是相同的。

Timer有3个要素:

定时时间:就是那个d
触发动作:就是那个f
时间channel: 也就是t.C

time.NewTimer()

NewTimer()创建一个新的计时器,该计时器将在其通道上至少持续d之后发送当前时间。它的返回值是一个Timer。

通过源代码我们可以看出,首先创建一个channel,关联的类型为Time,然后创建了一个Timer并返回。

  • 用于在指定的Duration类型时间后调用函数或计算表达式。
  • 如果只是想指定时间之后执行,使用time.Sleep()
  • 使用NewTimer(),可以返回的Timer类型在计时器到期之前,取消该计时器
  • 直到使用<-timer.C发送一个值,该计时器才会过期
package main
import (
    "fmt"
    "time"
)
func main() {
    /*
        1.func NewTimer(d Duration) *Timer
            创建一个计时器:d时间以后触发,go触发计时器的方法比较特别,就是在计时器的channel中发送值
    */
    //新建一个计时器:timer
    timer := time.NewTimer(3 * time.Second)
    fmt.Printf("%T\n", timer) //*time.Timer
    fmt.Println(time.Now())   //2019-08-15 10:41:21.800768 +0800 CST m=+0.000461190

    //此处在等待channel中的信号,执行此段代码时会阻塞3秒
    ch2 := timer.C     //<-chan time.Time
    fmt.Println(<-ch2) //2019-08-15 10:41:24.803471 +0800 CST m=+3.003225965

}

结果:

*time.Timer
2022-04-01 10:02:28.9274932 +0800 CST m=+0.000996601
2022-04-01 10:02:31.9275217 +0800 CST m=+3.001025101

timer.Stop (计时器停止)

示例代码:

package main
import (
    "fmt"
    "time"
)
func main() {
    //新建计时器,一秒后触发
    timer2 := time.NewTimer(5 * time.Second)
    //新开启一个线程来处理触发后的事件
    go func() {
        //等触发时的信号
        <-timer2.C
        fmt.Println("Timer 2 结束。。")
    }()
    //由于上面的等待信号是在新线程中,所以代码会继续往下执行,停掉计时器
    time.Sleep(3*time.Second)
    stop := timer2.Stop()
    if stop {
        fmt.Println("Timer 2 停止。。")
    }
}

结果:Timer 2 停止。。

time.After()

在等待持续时间之后,然后在返回的通道上发送当前时间。它相当于NewTimer(d).C。在计时器触发之前,垃圾收集器不会恢复底层计时器。如果效率有问题,使用NewTimer代替,并调用Timer。如果不再需要计时器,请停止。

示例代码:

package main
import (
    "fmt"
    "time"
)
func main() {
    /*
        func After(d Duration) <-chan Time
            返回一个通道:chan,存储的是d时间间隔后的当前时间。
    */
    ch1 := time.After(3 * time.Second) //3s后
    fmt.Printf("%T\n", ch1) // <-chan time.Time
    fmt.Println(time.Now()) //2019-08-15 09:56:41.529883 +0800 CST m=+0.000465158
    time2 := <-ch1
    fmt.Println(time2) //2019-08-15 09:56:44.532047 +0800 CST m=+3.002662179
}

select语句

select 是 Go 中的一个控制结构。select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

 

select语句的语法结构和switch语句很相似,也有case语句和default语句:

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s); 
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}

说明:

  • 每个case都必须是一个通信
  • 所有channel表达式都会被求值
  • 所有被发送的表达式都会被求值
  • 如果有多个case都可以运行,select会随机公平地选出一个执行。其他不会执行。
  • 否则:

               如果有default子句,则执行该语句。

               如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

示例代码:

package main
import (
    "fmt"
    "time"
)
func main() {
    /*
        分支语句:if,switch,select
        select 语句类似于 switch 语句,
            但是select会随机执行一个可运行的case。
            如果没有case可运行,它将阻塞,直到有case可运行。
    */
    ch1 := make(chan int)
    ch2 := make(chan int)
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- 200
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- 100
    }()
    select {
    case num1 := <-ch1:
        fmt.Println("ch1中取数据。。", num1)
    case num2, ok := <-ch2:
        if ok {
            fmt.Println("ch2中取数据。。", num2)
        }else{
            fmt.Println("ch2通道已经关闭。。")
        }
    }
}

运行结果:可能执行第一个case,打印100,也可能执行第二个case,打印200。(多运行几次,结果就不同了)

2) 使用 select 可以解决从管道取数据的阻塞问题

package main
import (
    "fmt"
    "time"
)
func main()  {
    intChan :=make(chan int,10)
    for i:=1;i<=10;i++{
        intChan<-i
    }
    strChan :=make(chan string,5)
    for i:=1;i<=5;i++{
        strChan<-"hello"+fmt.Sprintf("%d",i)
    }
    //传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
    //在实际开发中,可能我们不好确定什么时候关闭该管道
    for{
        select {
            //注意:这里,如果intChan一直没有关闭,不会一直阻塞而deadlock
            //会自动到下一个case匹配
            case v:=<-intChan :
                fmt.Printf("从intChan读取数据%d\n",v)
                time.Sleep(time.Second)
            case v:=<-strChan:
                fmt.Printf("从strChan读取数据%s\n",v)
                time.Sleep(time.Second)
            default:
                fmt.Printf("都取不到了,程序员可以加入自己的逻辑\n")
                return
        }
    }
}

3) goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题

   说明:如果我们起了一个协程,但是这个协程出现了panic 如果我们没有

   捕获这个panic,就会造成整个程序崩溃,这时我们可以在goroutine中使用

   recover来捕获panic 进行处理,这样即使这个协程发生了问题,但是主线程

   仍然不受影响,可以继续执行。

package main
import (
    "fmt"
    "time"
)
func sayHello()  {
    for i:=0;i<10;i++{
        time.Sleep(time.Second)
        fmt.Println("hello,world")
    }
}
func test()  {
    //错误处理
    defer func() {
        if err:=recover();err !=nil{
            fmt.Println("test 发生了错误=",err)
        }
    }()
    var myMap map[int]string
    myMap[0] = "golang" //错误的没有make
}
func main()  {
    go sayHello()
    go test()
    for i:=0;i<10;i++{
        time.Sleep(time.Second)
        fmt.Println("main() ok~")
    }
}

 二十七.反射

基本介绍

1) 反射可以在运行时动态获取变量的各种信息,比如变量的类型(type),类别(kind)

2) 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段,方法)

3) 通过反射,可以修改变量的值,可以调用关联的方法

4) 使用反射,需要import("reflect")

package reflect

reflect包实现了运行时反射,允许程序操作任意类型的对象。典型用法是用静态类型interface{}保存一个值,通过调用TypeOf获取其动态类型信息,该函数返回一个Type类型值。

调用ValueOf函数返回一个Value类型值,该值代表运行时的数据。Zero接受一个Type类型参数并返回一个代表该类型零值的Value类型值。

1) reflect.TypeOf(变量名),获取变量的类型,返回reflect.Type类型

2) reflect.ValueOf(变量名),获取变量的值,返回reflect.Value类型reflect.Value 是一个结构体类型[文档],通过reflect.Value 可以获取到

关于该变量的很多信息。

3) 变量,interface{}和 reflect.Value 是可以相互转换的,这点在实际开发中,会经常使用到   (reflect.Value转换为空接口类型用函数:func (v Value) Interface() (i interface{}) 再转换为具体类型用 类型断言

 

基本数据类型 interface{} reflect.Value 进行反射的基本操作

package main
import (
    "fmt"
    "reflect"
)
func reflectTest01(b interface{})  {
   //通过反射获取传入的变量的  type kind 值
   rType := reflect.TypeOf(b)
   fmt.Printf("rType=%v, type=%T\n",rType,rType) //rType=int, type=*reflect.rtype
   rVal :=reflect.ValueOf(b)
    fmt.Printf("rVal=%v, type=%T\n",rVal,rVal) //rVal=100, type=reflect.Value
    n1 :=10
    n2 := n1 + int(rVal.Int())//转换为真正的值,返回的是int64 需要转换一下
    fmt.Println("n1+n2的结果:",n2) //n1+n2的结果: 110
    //将rVal转换为 interface{}
    iv := rVal.Interface()
    num2 :=iv.(int)//类型断言
    fmt.Println("num2=",num2)
    fmt.Println(num2+20)//为int类型可以直接相加
}
func main()  {
    //基本类型
    var num int = 100
    reflectTest01(num)
}

结构体类型 interface{} reflect.Value 进行反射的基本操作

package main
import (
    "fmt"
    "reflect"
)
func reflectTest02(b interface{})  {
    //1.获取reflect.Type
    rType := reflect.TypeOf(b)
    fmt.Printf("rType=%v, type=%T\n",rType,rType) //rType=main.Student, type=*reflect.rtype
    //2.获取reflect.Value
    rVal :=reflect.ValueOf(b)
    fmt.Printf("rVal=%v, type=%T\n",rVal,rVal) //rVal={tom 20}, type=reflect.Value
    //3.获取 变量对应的kind
    kind := rVal.Kind()//或者 rType.Kind()
    fmt.Printf("kind=%v, type=%T\n",kind,kind) //kind=struct, type=reflect.Kind
    //将rVal转换为 interface{}
    iv := rVal.Interface()
    fmt.Printf("iv = %v iv type = %T\n",iv,iv)//iv = {tom 20} iv type = main.Student
    stu,ok :=iv.(Student)//类型断言 多个可以用 switch
    if ok{
        fmt.Println("stu.Name",stu.Name)//stu.Name tom
    }
}
type Student struct {
    Name string
    Age int
}
func main()  {
    //结构体
    stu :=Student{"tom",20}
    reflectTest02(stu)
}

1) reflect.Value.Kind,获取变量类别,返回的是一个常量[手册]

Kind代表Type类型值表示的具体分类。零值表示非法分类。

2) Type是类型,Kind是类别,Type和kind可能相同的,也可能是不同的。

比如:var num int = 10 num的Type是int,kind也是int

比如:var stu Student stu的Type是 包名.Student,kind是struct

3) 通过反射可以在让变量在 interface{} 和 Reflect.Value 之间相互转换

4) 使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配,比如x是int,那么就应该使用reflect.Value.Int(),而不能使用其他的,否则panic

5) 通过反射的来修改变量,注意当使用SetXxx 方法来设置需要通过对应的指针类型来完成,这样才能改变传入的变量的值,
同时需要使用到reflect.Value.Elem()方法

package main
import (
    "fmt"
    "reflect"
)
func reflect01(b interface{})  {
    //2.获取reflect.Value
    rVal :=reflect.ValueOf(b)
    fmt.Println("rVal kind=",rVal.Kind()) //rVal kind= ptr 是一个指针
    //Elem返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装。
    //如果v的Kind不是Interface或Ptr会panic;如果v持有的值为nil,会返回Value零值。
    rVal.Elem().SetInt(20)
}
func main()  {
    num := 10
    reflect01(&num)
    fmt.Println(num)
}

 该方法Method 方法的排序是按照方法第一个字母的 ASCII码 排序的

1) 使用反射来遍历结构体的字段,调用结构体的方法,并获取结构体标签的值

package main
import (
    "fmt"
    "reflect"
)
//定义了一个 Monster 结构体
type Monster struct {
    Name  string  `json:"name"`
    Age   int     `json:"monster_age"`
    Score float32 `json:"成绩"`
    Sex   string
}
//方法,返回两个数的和
func (s Monster) GetSum(n1, n2 int) int {
    return n1 + n2
}
//方法, 接收四个值,给 s 赋值
func (s Monster) Set(name string, age int, score float32, sex string) {
    s.Name = name
    s.Age = age
    s.Score = score
    s.Sex = sex
}
//方法,显示 s 的值
func (s Monster) Print() {
    fmt.Println("---start~----")
    fmt.Println(s)
    fmt.Println("---end~----")
}
func TestStruct(a interface{}) {
    //获取 reflect.Type 类型
    typ := reflect.TypeOf(a)
    //获取 reflect.Value 类型
    val := reflect.ValueOf(a)
    //获取到 a 对应的类别
    kd := val.Kind()
    //如果传入的不是 struct,就退出
    if kd != reflect.Struct {
        fmt.Println("expect struct")
        return
    }
    //获取到该结构体有几个字段
    num := val.NumField()
    fmt.Printf("struct has %d fields\n", num) //4
    // 变量结构体的所有字段
    for i := 0; i < num; i++ {
        fmt.Printf("Field %d: 值为=%v\n", i, val.Field(i))
        //获取到 struct 标签, 注意需要通过 reflect.Type 来获取 tag 标签的值
        tagVal := typ.Field(i).Tag.Get("json")
        // 如果该字段于 tag 标签就显示,否则就不显示
        if tagVal != "" {
            fmt.Printf("Field %d: tag 为=%v\n", i, tagVal)
        }
    }
    //获取到该结构体有多少个方法
    numOfMethod := val.NumMethod()
    fmt.Printf("struct has %d methods\n", numOfMethod)
    //方法的排序默认是按照 函数名的排序(ASCII 码)
    val.Method(1).Call(nil) //获取到第二个方法。调用它
    //调用结构体的第 1 个方法 Method(0)
    var params []reflect.Value
    //声明了 []reflect.Value
    params = append(params, reflect.ValueOf(10))
    params = append(params, reflect.ValueOf(40))
    res := val.Method(0).Call(params) //传入的参数是 []reflect.Value, 返回[]reflect.Value
    fmt.Println("res=", res[0].Int()) //返回结果, 返回的结果是 []reflect.Value*/
}
func main() {
    //创建了一个 Monster 实例
    var a Monster = Monster{Name: "黄鼠狼精", Age: 400, Score: 30.8, Sex: "母的"}
    //将 Monster 实例传递给 TestStruct 函数
    TestStruct(a)
}

time包中的通道相关函数

var str string ="hello 你好"
str1 := []rune(str)
for i:=0;i<len(str1);i++{
fmt.Printf("%c\n",str1[i])
}
//如果要获取str的字符串长度,而不是按字节长度计算 len是按字节长度计算的
fmt.Println("str 的长度:",utf8.RuneCountInString(str))
//通过rune类型处理unicode字符
fmt.Println("str 的长度: rune:",len([]rune(str)))
/*golangbyte数据类型与rune相似,它们都是用来表示字符类型的变量类型。它们的不同在于:

byte 等同于int8,常用来处理ascii字符
rune 等同于int32,常用来处理unicodeutf-8字符*/