golang基础知识

Go 学习笔记

一、Go常用命令

通过go help 可以查看所有的go命令

  • build : 编译包和依赖;如果是main 包,当执行 go build 之后,会在当前目录下生成一个可执行文件。如果需要再$GOPATH/pkg 目录下生成相应的文件,需要执行go install ,或者使用go build -o path/main.go 代码路径,示例go]$ go build -o bin/sort src/day4/sort/test1/main.go

  • run:编译并运行go程序

  • get :下载并安装包和依赖,-u 强制使用网络去更新包;

  • clean: 移除对象文件;用来移除当前源码包和关联源码包里面编译生成的文件;go clean -i -n

  • doc :显示包或者符合的文档

  • env:打印go 的环境变量信息

  • bug:启动错误报告

  • fix:运行 go tool fix

  • fmt :运行gofmt 进行格式化,gofmt -w -l src ; 开发工具里面一般自带格式化功能

  • install : 编译并安装包和依赖

  • list:列出包

  • test:运行测试

  • tool : 运行go提供的工具

二、注释

  • 在代码上增加注释,方便程序代码的阅读理解
package main

import "fmt"

// 注释  注释不参与程序的编译, 可以帮助理解程序功能
// 行注释  只能注释一行
/*
	块注释
	可以注释多行
*/

// main 叫做主函数 是程序的主入口  程序有且只有一个主函数
func main() {
	// 在终端打印 hello Go
	fmt.Println("hello Go")
}

三、命名规则

go语言是一门区分大小写的语言

命名规则设计变量、常量、全局函数、结构体、接口、方法等的命名,任何需要对外暴露的名字必须一大写字母开头、不需要对外暴露的则应该以小写字母开头。

当命名(常量、变量、类型、函数命、结构体字段)以一个大写字母开头,eg:Name 那么这种就可以被外部包的代码所引用(引用时需要先导入这个包)

包名称

保持package的名字和目录保持一致,尽量采取有意义的包名,包名应该为小写单词,不要使用下划线或者混合大小写

package model
package service

文件命名

采取有含义的文件名,使用小写字母,使用下划线分割组合单词

user_dao.go
admin.go

结构体命名

采用驼峰命名法,首字母根据访问控制大写或者小写

type Info struct{
  Name string
  Age int
}

接口命名

采用驼峰命名法

单个函数的结构名以“er”作为后缀,eg:ReaderWriter

type Usber interface {
  Read()
  Write()

变量命名规范

1、只能以字母或者下划线开头

2、只能使用字母数字下划线

3、区分大小写

4、不能使用系统关键字

breakdefaultfuncinterfaceselectcasedefergomapstructchanelsegotopackageswitchconstfallthroughifrangetypecontineforimportreturnvar

建议使用驼峰命名法 定义变量名称

  • 小驼峰式命名法:第一个单词以小写字母开头,第二个单词的首字母大写,例如:myName
  • 大驼峰式命名法:每一个单词的首字母都采用大写字母,例如:MyName

还有一种流行的命名方法,使用 _ 来链接所有的单词,例如:my_name

常量命名

常量需全部大写字母组成,并使用下划线分词

const APP_URL = "cc.com"

单元测试

单元测试文件命名规范为example_test.go, 测试用例的函数必须以 Test开头,eg:TestExample

四、变量

什么叫变量

  • 所谓的变量简单的理解就是计算机用来存储数据的。

  • 变量就是一个指定名称和类型的数据存储位置。

  • 变量的值在运行中是可以改变的

变量定义格式

var 变量名 数据类型 声明变量

var 变量名 数据类型 = 值 定义

变量名 := 值 自动推到类型

注意: 变量的类型不同不能进行计算,需要使用类型转换

变量赋值

package main

// 定义全局变量
var x string = "Hello Go"

func main() {
  // 定义内部变量
  var y string = "vic"

  // 等价以下写法
  var y string
  y = "vic"

  // 初始化赋值
  var name string = "teacher"
  
  // 或者直接赋值,让GO语言推断变量的类型
  var y = "vic"

  // 更简洁的写法,自动推到类型,:= ;也是最常用的方法, := ,此方法初始化变量的方式只能用在函数内部
  x := "victor"


  // 一次定义初始化多个变量
  var (
  	v1 int = 20
    v2 string = "vic"
  )
  // 等价于
  v1, v2 := 20, "vic"


 // 定义匿名变量
 // _ 下划线定义,匿名变量配合函数返回值使用才有价值,一般是丢弃数据不进行处理
  _, x := 1, 2
}

输出格式

fmt 包 实现了类似 C 语言 printf 和 scanf 的格式化I/O(输出输入)

https://studygolang.com/static/pkgdoc/pkg/fmt.htm#Formatter

func Println(a ...interface{})(n int, err error)

func Fprintln(w io.Writer, a ...interface{}) (n int, err error)
  • fmt.Println() 输出数据, 自带换行

  • fmt.Print() 输出数据,不带换行

  • fmt.Printf() 格式化输出数据

    • %d 整型
    • %s 直接输出字符串
    • %% 百分号
    • %t 布尔类型
    • %v 值得默认格式
    • %+v 类似%v,但输出结构体时会添加字段名
    • %#v 值的 Go 语法表示
    • %T 值的类型的 Go 语法表示
    • %q 该值对应的双引号括起来的 go 语法字符串字面值
    • %c 该值对应的 unicode 码值或者[]byte
    • %f 浮点数输出; // %.2f 默认宽度,精度 2, 32.24%
    • unsafe.Sizeof§ 可以返回 p 变量 占用的字节数 , 例如: fmt.Printf(“p 的字节数是: %d”, unsafe.Sizeof§)
  • fmt.Fprintln() 、fmt.Fprint、fmt.Fprintf 功能同上面三个函数,只不过将转换结果写入到 w 中

  • fmt.Scan() 输入数据 &变量

常用的转义字符

\t	一个指标单位,实现对齐功能
\n   实现换行
\\	一个 \
\"	一个 “
\r	一个回车

五、基础数据类型

1、布尔类型

bool 默认值 false ,其值不为真即为假,不可以用数字代表,true 或者 false

2、浮点型

float32 默认小数位置保留 7 位有效数据

float64 默认小数位置保留 15 位有效数据

3、字符类型

没有单独的字符型,使用 byte 来保存单个字母的字符

字符 一般使用单引号,只有一个字符,转义字符除外\n,打印值为 ASCII

4、字符串类型
  • 字符串一旦赋值就不能修改,由一个或多个字符组成

  • “” 双引号,会识别转义字符

  • `` 反引号,以字符串原生形式输出,包括换行和特殊字符

注意: 在 Go 语言中一个汉字算作 3 个字符

字符串拼接

package main

import (
	"bytes"
	"fmt"
	"strings"
)

func main() {
	name := "王小二"
	age := "20"
	// 字符串拼接
	str := name + ", " + age
	fmt.Println(str)

	// 利用strings.join 拼接, 把字符串转换成数组[]string
	str1 := strings.Join([]string{name, age}, " - ")
	fmt.Println(str1)

	// 利用 buffer.WriteString() 拼接
	var buff bytes.Buffer
	buff.WriteString("王小二")
	buff.WriteString(" , ")
	buff.WriteString("20")
	fmt.Println(buff.String())
}

字符串修改

在go语言中不能直接修改字符串的内容,一般先将字符串复制到一个可写的变量中(为[]byte、[]rune 的变量类型)再进行修改

  • byte 类型只能正常输出 ASCII 编码范围的字符,byte 表示一个字节
  • rune 类型可以输出 UTF-8 编码范围的字符,rune 表示四个字节
package main

import (
	"fmt"
	"strings"
)

func main() {
	str := " Beijing is 首都, Welcome "

	// 对于全是ASCII编码的字符串用 ([]byte)
	strTmp := []byte(str)       // 先转换为 []byte 类型
	strTmp[3] = ','             // 把第 3 个字符,修改成 逗号
	fmt.Println(string(strTmp)) // string()表示强制类型转换,转换为字符串

	// 利用strings.LastIndex 查询索引值
	str1 := []byte(str)
	a := strings.LastIndex(str, "i") // 把最后一个 i 修改成 +
	str1[a] = '+'
	fmt.Println(string(str1))

	// 对于包含中文等字符的字符串时用 ([]rune)
	a2 := strings.Index(str, "首") // 修改中文,先转换成 []rune
	str2 := []rune(str)
	str2[a2] = '中'
	fmt.Println(string(str2))

}

字符串反转

package main

import "fmt"

func main() {
	str := " Beijing is , Welcome "

	// 先转换成字节[]byte
	args := []byte(str)

	// 定义一个新的变量
	str2 := ""
	// 循环打印args 字节,并追加到 str2
	for i := len(args) - 1; i >= 0; i-- {
		str2 += string(args[i])
	}

	fmt.Println(str2)
}

strings 包

package main

import (
	"fmt"
	"strings"
)

func main() {
	str := " Beijing is 首都, Welcome "
	fmt.Println(str)

	// strings.Contains 布尔判断,返回 true 、false
	fmt.Println(strings.Contains(str, "co"))

	// strings.Count 统计某个 字符 出现的次数
	fmt.Println(strings.Count(str, "i"))

	// strings.TrimSpace 去掉字符串 首尾的空格
	fmt.Println(strings.TrimSpace(str))

	// strings.Split 对字符串进行分割,返回数组切片
	fmt.Println(strings.Split(str, ","))

	// strings.Replace 字符串替换,替换次数为n 次,n 从 1 开始计算如果n 为 -1 ,则替换全部
	fmt.Println(strings.Replace(str, "Beijing", "Shanghai", 1))

	// strings.ToLower 全部转换为小写
	fmt.Println(strings.ToLower(str))

	// strings.ToUpper 全部转换成大写
	fmt.Println(strings.ToUpper(str))

	// strings.HasPrefix 判断是否以某字符开头,布尔判断,返回 true 、false
	fmt.Println(strings.HasPrefix(str, ""))

	// strings.HasSuffix 判断是否以某字符开头,布尔判断,返回 true 、false
	fmt.Println(strings.HasSuffix(str, "1"))

	// strings.TrimLeft 去掉字符串首部 定义的字符
	fmt.Println(strings.TrimLeft(str, " "))

	// strings.TrimRight 去掉字符串尾部 定义的字符
	fmt.Println(strings.TrimRight(str, " "))
	
  // strings.Index 查看字符所在的第一个索引出现的位置
	fmt.Println(strings.Index(str, "i"))

	// strings.LastIndex 查看字符所在的最后一个索引出现的位置
	fmt.Println(strings.LastIndex(str, "i"))
}

5、数字类型

intuint 在32位操作系统上,使用 32 位(4个字节),64位系统上使用64位(8个字节)

整数

  • int8 (-128 <-> 127)
  • int16 (-32768 <-> 32767)
  • int32 (-2147483648 <-> 2147483647)
  • int64 ()

无符号整数

  • uint8 (0 <-> 255)
  • uint16 (0 <-> 65535)
  • uint32 (0 <-> 4294967295)
  • uint64 (0 <-> 18446744073709551615)

查看数据类型,以及最小~最大值

package main

import (
	"fmt"
	"math"
	"unsafe"
)

func main() {
	var i8 int
	fmt.Printf("i8类型:%T \ni8长度:%d \n最小值 %v \n最大值 %v \n", i8, unsafe.Sizeof(i8), math.MinInt, math.MaxInt)
}

6、常量
  • 程序运行期间不能修改,声明需要 const
  • 常量在定义的时候必须要有初始值
  • 以大写字母开头的常量在包外是可见的,否则为包内私有
  • 变量的类型推断方式 := 不能用来定于常量

示例:

// 常规定义
const X string = "vic"
const y = "tom"

// 一次定义多个
const x,y int = 1, 2

// 一般使用如下方式定义多个常量
const (
	x int = 10
  y string = "vic"
)

// 如果不提供初始类型,那么视作与上一个常量值相同
const(
	a = "vic"
  b  // b = "vic"
)
7、iota 枚举
  • 常量声明可以使用iota 常量生成器初始化

  • const 中每新增一行常量声明将使 iota 计数一次

    示例

    // 第一个 iota 等于 0 ,每当 iota 在新的一行被使用时,它的值都会自动加 1 ;所以 a=0, b=1, c=2 可以简写如下
        const(
          a = iota
          b
          c
        )
    
    		// 枚举 常见的iota示例
        //关键字 iota 定义常量组中从 0开始按行计数的自增枚举值
        const (
            Sunday = iota // 0
            Monday // 1,通常省略后续行表达式。
            Tuesday // 2
            Wednesday // 3
            Thursday // 4
            Friday // 5
            Saturday // 6
        )
    
    fmt.Println(Sunday)
    
        // 使用 _ 跳过某些值
        const (
            n1 = iota  // 0
            n2      // 1
            _
            n4      // 3
     		 )
    	  // 等价于使用_ 跳过某个值,自定义数值
        const (
            n1 = iota  // 0
            n2      // 1
            n3 = 100
            n4      // 3
     		 )
    

四、运算符

1、算术运算符

示例:

假定 A 值为 10 ,B 值为 20

运算符描述实例
+相加A + B 输出结果 30
-相减A - B 输出结果 -10
*相乘A * B 输出结果 200
/相除B / A 输出结果 2
%求余B % A 输出结果 0
++自增A++ 输出结果 11
自减A-- 输出结果 9
2、关系运算符

示例:

假定 A 值为 10 ,B 值为 20

运算符描述实例
==检查两个值是否相等,如果相等返回 True 否则返回 False。(A == B) 为 False
!=检查两个值是否不相等,如果不相等返回 True 否则返回 False。(A != B) 为 True
>检查左边值是否大于右边值,如果是返回 True 否则返回 False。(A > B) 为 False
<检查左边值是否小于右边值,如果是返回 True 否则返回 False。(A < B) 为 True
>=检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。(A >= B) 为 False
<=检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。(A <= B) 为 True
3、逻辑运算符

示例:

假定 A 值为 True ,B 值为 False

运算符描述实例
&&逻辑 AND 运算符。 如果两边的操作数都是 True,则条件 True,否则为 False。(A && B) 为 False
||逻辑 OR 运算符。 如果两边的操作数有一个 True,则条件 True,否则为 False。(A || B) 为 True
!逻辑 NOT 运算符。 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。!(A && B) 为 True
4、赋值运算符
运算符描述实例
=简单的赋值运算符,将一个表达式的值赋给一个左值C = A + B 将 A + B 表达式结果赋值给 C
+=相加后再赋值C += A 等于 C = C + A
-=相减后再赋值C -= A 等于 C = C - A
*=相乘后再赋值C _= A 等于 C = C _ A
/=相除后再赋值C /= A 等于 C = C / A
%=求余后再赋值C %= A 等于 C = C % A
5、其他运算符
运算符描述实例
&返回变量存储地址&a; 将给出变量的实际地址。
*指针变量。*a; 是一个指针变量

五、流程控制

  • 选择结构:程序依据是否满足条件,有选择的执行响应功能

  • 循环结构: 程序依据条件是否满足,循环多次执行某段代码

1、选择结构

if 语句

if 布尔表达式 {
  /* 在布尔表达式为 True 时执行  */
}

if…else 语句

if 布尔表达式 {
  /* 在布尔表达式为 True 时执行  */
} else {
  /* 在布尔表达式为 False 时执行  */
}

if 语句嵌套

在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句

if 布尔表达式 1 {
   /* 在布尔表达式 1 为 true 时执行 */
   if 布尔表达式 2 {
      /* 在布尔表达式 2 为 true 时执行 */
   }
}

示例

// 根据布尔值 判断
var flag1 = true
if flag1 {
  fmt.Println("flag1 is true")
}
fmt.Println("判断结束")


a := 10
b := 20
if b > a {
  fmt.Printf("b: %d", b)
} else {
  fmt.Printf("a: %d", a)
}


// if 里面可以在条件判断语句里面声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了
  if x :=1 ; x > 10 {
      fmt.Println("x is greater than 10")
    } else {
      fmt.Println("x is less than 10")
    }

	//
	c := 6
	if c > 0 && (10/c) >1 {
		fmt.Println("OK!")
	} else {
		fmt.Println("error!")
	}


// 多条件判断
	OS := "win"
	if OS == "Centos" {
		fmt.Printf("this system os: %s \n", OS)
	} else if OS == "Ubuntu" {
		fmt.Printf("this is system os: %s \n", OS)
	} else {
		fmt.Printf("this is system os: %s \n", OS)
	}



 // 嵌套案例; 三只小猪称体重

	a, b, c := 30, 20, 10

	if a > b {
		if a > c {
			fmt.Println("a 最重", a)
		} else {
			fmt.Println("c 最重", c)
		}
	} else {
		if b > c {
			fmt.Println("b 最重", b)
		} else {
			fmt.Println("c 最重", c)
		}
	}

switch 语句

  • 用于基于不同条件执行不同的动作,每一个case分支都是唯一的,从上至下逐一测试,直到匹配为止
  • 执行的过程从上至下,直到找到匹配项,匹配项后面也不需要加break
  • 匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough
switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}

示例:

// 单独写上面赋值,或者写在switch 里面赋值,再判断,二者选其一
	x := 2
	switch x {
	case 1:
		fmt.Println("1")
	case 2:
		fmt.Println("2")
	default:
		fmt.Println("6")
	}


// switch 不提供任何判断的值,在每个case分支做测试判断
switch Num :=20; {
  case Num < 0:
  	fmt.Println("Num < 0")
  case Num > 0 && Num < 10:
  	fmt.Println("Num 值很小")
  default:
  	fmt.Println("Num 值很大")
}

select 语句

  • 类似于switch语句,但是每个 case必须是一个通信操作,要么是发送要么是接收
  • select随机执行一个可运行的case 。如果没有case可执行,它将阻塞,直到有case可运行。一个默认的子句应该是总是可以运行的

语法

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

示例:

var c1, c2, c3 chan int
   var i1, i2 int
   select {
      case i1 = <-c1:
         fmt.Printf("received ", i1, " from c1\n")
      case c2 <- i2:
         fmt.Printf("sent ", i2, " to c2\n")
      case i3, ok := (<-c3):  // same as: i3, ok := <-c3
         if ok {
            fmt.Printf("received ", i3, " from c3\n")
         } else {
            fmt.Printf("c3 is closed\n")
         }
      default:
         fmt.Printf("no communication\n")
   }
2、循环结构
# 其中expression1 和expression3 是变量声明或者函数调用返回值,
# expression2 是用来条件判断,
# expression1 在循环开始之前调用,
# expression3 在每轮循环结束之时调用

for expression1; expression2; expression3 {
    // ...
}

示例

// 循环条件初始化 条件判断,循环后条件变化
	for a := 0; a < 5; a++ {
		fmt.Printf("a is result: %d \n", a)
	}


// for 配合 range 可以用于读取 slice 和 map 的数据 for 循环迭代数组
for i,x :=range num {
		fmt.Printf("%d is result: %d \n", i, x)
	}

strings := []string{"google", "runoob"}
for i, s := range strings {
    fmt.Println(i, s)
}

numbers := [6]int{1, 2, 3, 5}
for i,x:= range numbers {
     fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
}


	// 打印九九乘法表
	// 外层控制行,内行控制列
	for k := 1; k <= 9; k++ {
		for j := 1; j < k; j++ {
			fmt.Printf("%d*%d = %d  ", j, k, k*j)
		}
		fmt.Println()
	}
**break 语句 **

Break 操作是跳出当前循环,可用于 for、switch、select

	i := 0
	for { // for 后面不写任何东西,循环条件永远为真,也就是死循环
		i++
		time.Sleep(time.Second)  // 间隔 1s 打印一次
		if i == 6 { //跳出循环,如果嵌套多个循环,,就跳出最近的那个内循环
			break
		}
		fmt.Println("i = ", i)
	}
**continue 语句 **

continue 操作是跳过本次结构继续往下执行, 仅用于 for 循环语句

	i := 0
	for {
		i++
		time.Sleep(time.Second)

		if i == 3 {
			continue
		}
		fmt.Println("i = ", i)
	}

流程控制关键字goto

goto语句通过标签进行代码之间的无条件跳转

// 跳出双层循环
	/*
		 标签breakTag 只能被goto引用,不影响代码执行流程,
		在定义breakTag标签之前有个return语句,
		此处如果不手动返回,则在不满住条件时也会执行breakTag代码
	*/
for i := 0; i < 5; i++ {
		for j := 0; j < 5; j++ {
			if i == 3 && j == 3 {
				goto breakTag
			}
			fmt.Println(i, " -- ", j)
		}
	}
	return
breakTag:
	fmt.Println("跳出循环.....")

六、函数

函数调用流程:先调用后返回,先进后出

1、函数的定义

语法

func 函数名(参数列表) (返回值) {
		函数体
}

特点

  • 不支持重载,一个包不能包涵同名的函数
  • 函数也是一种类型,一个函数可以赋值给变量
  • 匿名函数
  • 多返回值

定义函数名,但是参数和返回值均为空

package main

import "fmt"

// 无参无返回值函数的定义
func sayHello() {
	fmt.Println("SayHello")
}

// main 叫做主函数 是程序的主入口  程序有且只有一个主函数
func main() {
	sayHello()
}

定义函数名及参数,返回值为空

package main

import "fmt"

// 有参无返回值函数的定义
func sayHello(a int, b int) {
	fmt.Println(a + b)
}

// main 叫做主函数 是程序的主入口  程序有且只有一个主函数
func main() {
	sayHello(1, 2)
}

定义函数名及参数,返回一个返回值

package main

import "fmt"

// 有参有返回值函数的定义
func max(num1, num2 int) int {
	// 定义局部变量
	var result int
	if num1 > num2 {
		result = num1
	} else {
		result = num2
	}
	return result
}

// main 叫做主函数 是程序的主入口  程序有且只有一个主函数
func main() {
	// 调用函数并返回最大值
	ret := max(10, 20)
	fmt.Println(ret)
}

定义函数名及参数,且定义命名返回值

package main

import "fmt"

// 定义函数名及参数,且定义一个命名返回值
func add(a, b int) (sum int) {
	sum = a + b
	return
}

// 定义函数名及参数,且定义二个命名返回值
func cacl(a, b int) (sum int, cut int) {
	sum = a + b
	cut = a - b
	return
}

// main 叫做主函数 是程序的主入口  程序有且只有一个主函数
func main() {
	ret1 := add(1, 2)
	fmt.Println(ret1)

	ret2, ret3 := cacl(3, 9)
	fmt.Println(ret2, "\n", ret3)
}

注意 :

在上面的函数中,sum 和 cut 是命名返回值,return 语句没有指定任何返回值。因为在函数声明的时候已经指定 sum 和 cut 是返回值,在遇到 return 语句时它们会自动从函数中返回,在 Go 语言中,有返回值的的函数,无论是命名返回值还是普通返回值,函数中必须包涵 return 语句

有函数名,定义可变参数

  • 参数里面arg 是一个slice (切片)

  • 通过arg(index)依次访问所有参数

  • 通过len(arg) 来判断传递参数的个数

    // 0 个或 多个参数
    func add(arg...int) int{
      return
    }
    
    // 1 个或 多个参数
    func add(a int, arg...int) int{
      return
    }
    
    // 2 个或 多个参数
    func add(a, b int, arg...int) int{
      return
    }
    

示例

package main

import (
	"fmt"
)

// 传递值进行相加
func add(a int, arg...int) int {
	sum := a
	for i :=0; i < len(arg); i++ {
		//打印 后面参数的值
		fmt.Printf("可变参数: %d\n",arg[i])
		sum +=arg[i]
	}
	return sum
}

// 传递字符串,结果拼接
func connect(a string, arg...string) (reslut string) {
	reslut = a
	for i := 0; i < len(arg); i++ {
		reslut +=arg[i]
	}
	return
}

func main() {
	sum := add(10, 20, 30)
	fmt.Printf("result is :%d\n", sum)

	res :=connect("Hi", " ", "Victor")
	fmt.Println(res)
}

函数也是一种类型

package main

import (
	"fmt"
)

// 在go 中,函数也是一种数据类型
// 可以赋值给一个变量,则该变量就是一个函数类型的变量了,通过变量可以对函数进行调用
func sum(a, b int) int {
	return a + b
}

func main() {
	c := sum
	fmt.Printf("c 的类型是:%T \nsum 的类型是:%T \n", c ,sum)

	res := c(1,2) // 等价  res := sum(1,2)
	fmt.Println("res 的值是: ",res)
}

2、高级函数

函数作为参数传递给另一个函数

package main

import "fmt"

// 定义一个函数
func sayHello(name string) {
	fmt.Printf("Hello %s", name)
}

// 定义函数,传参类型为函数类型
func test(name string, f func(string)) {
	f(name)
}
func main() {
	test("Tom", sayHello)
}

函数作为函数的返回值

package main

import "fmt"

func add(a int, b int) int {
	return a + b
}

func sub(a int, b int) int {
	return a - b
}

func other(a int, b int) int {
	fmt.Println("传递参数不对")
	return 1001
}

func cal(s string) func(int, int) int {
	switch s {
	case "+":
		return add
	case "-":
		return sub
	default:
		return other
	}
}

func main() {
	add := cal("+")
	r := add(3, 5)
	fmt.Println(r)

	sub := cal("-")
	r = sub(3, 5)
	fmt.Println(r)
  
  other := cal("/")
	r := other(3, 5)
	fmt.Println(r)

}

3、匿名函数
  • 匿名函数即没有名字的函数

  • 如果某个函数只是希望调用异常,那么就可以使用匿名函数,当然也可以实现多次调用

示例

package main

import "fmt"

// main 叫做主函数 是程序的主入口  程序有且只有一个主函数
func main() {
	// 无参无返回值的匿名函数
	func() {
		fmt.Println("this is 匿名函数")
	}() // 末尾使用 () 表示此匿名函数被自己调用


	// 无参有返回值的匿名函数
	func() int {
		a := 10
		fmt.Println(a)
		return a
	}()


	// 匿名函数:自己调用自己;二个数求和,先赋值给一个变量再调用
	ret := func(a1, a2 int) int {
		return a1 + a2
	}(10, 20)
	fmt.Printf("ret type => %T, ret=%d \n", ret, ret)


	// 第二种匿名函数定义方式
	f := func(a1, a2 int) int {
		return a1 - a2
	}
	f1 := f(10, 5)
	fmt.Printf("f type => %T, f1 type => %T \n", f, f1)
	fmt.Println(f1)
}

4、递归函数
  • 当一个函数在其函数体内调用自身,则称为递归函数
  • 必须定义函数的退出条件,如果没有退出条件,递归将成为死循环

示例一

func recusive(){
  fmt.Println("Hello ")
  time.Sleep(time.Second)
  recusive() //调用函数自身,实现递归
}

func main(){
  recusive
}

示例二

func test(n int){
  if n > 5 {
    n--   // 递归必须向退出条件逼近,否则就是无限循环调用
    test(n)
  }else{
    fmt.Println("n => ",n)
  }
}

func main(){
  test(5)
}

示例三

// 功能 阶乘 4 * 3 * 2 * 1
func mul(n int) int {
  // 返回条件
	if n == 1 {
		return 1
  } else{
    // 自己调用自己
    return mul(n-1) * n
  }
}

func main() {
	n := mul(4)
	fmt.Println(n)
}

示例四

// 递归实现数字累加
//递归实现1+2+3+……100
func add1(n int) int {
	if n == 100 {
		return 100
	}
	return n + add1(n + 1)
}

func add2(i int) (sum int) {
	if i == 1 {
		return 1
	}
	return i + add2(i - 1)
}

func main() {
	sum1 := add1(1)
	fmt.Printf("sum1 = %d\n", sum1)

	sum2 := add2(100)
	fmt.Printf("sum2 = %d", sum2)
}

5、init 函数

Golang 有一个特殊的函数 init 函数,先于 main函数执行,实现包级别的一些初始化操作

  • init 函数优先于main 函数执行,不能被其他函数调用

  • 每个包可以有多个init 函数,同一个包的 init 执行顺序,没有明确规定

  • init 函数没有参数 和 返回值

golang初始化顺序:变量初始化 > init() > main()

package main
import (
    "fmt"
)

var age = test()

func test() int(
    fmt.Println("test()") //  1
    return 20
)

// 通常在init()函数中完成初始化工作
func init() {
   fmt.Println("init()") //  2
}

func main() {
    fmt.Println("main()") //  3
}

七、复合类型

1、数组

用来存储集合的数据

数组的声明和初始化

1、声明存储数据的类型

2、存储元素的数量,也就是数组的长度

3、数组是类型相同元素的集合,Go 不允许在数组中混合使用不同类型的元素

4、数组中的所有元素都被自动赋值为元素类型的 0 值,布尔类型是 false,字符串是空字符串

5、数组的索引从 0 开始到 length-1 结束

数组的定义

  • 第一种方式

    var <数组名称> [<数组长度>]<数组元素类型>
    
    示例:
     // arr[0] 表示数组 arr 的第一个元素,arr[1] 表示数组的第二个元素,等等依次增加
    	var arr [5]
    	arr[0] = 4
      arr[1] = 43
      arr[2] = 3423
      arr[3] = 234
      arr[4] = 654
    
  • 第二种方式

    var <数组名称> = [<数组长度>]<数组元素类型>{元素1, 元素2, ...}
    
    示例:
    	var arr = [3]{32, 323, 765}
    
    GO提供了 := 操作符,可以在创建数组的时候直接初始化赋值
    arr := [3]{32, 323, 765}
    
  • 第三种方式

    声明数组的时候可以忽略数组的长度使用 ... 代替,让编辑器自动推到数组的长度
    	var <数组名称> = [...]<数组元素类型>{元素1, 元素2, ...}
    
    示例:
    	var arr = [...]{32342, 324, 43, 443}
    
      或者
        arr := [...]{32342, 324, 43, 443}
    
  • 第四种方式

    给数组指定的索引指定初始化的值,其他为 0
    	var <数组名称> = [...]<数组元素类型>{索引1:元素1, 索引2:元素2, ...}
    
    示例:
    	var arr = [...]{3:232, 6:4353}
    
    或者
    	arr := [...]{3:232, 6:4353}
    

示例:

循环打印数组中的值

package main

import (
	"fmt"
)

func main() {
	// 循环打印数组中的值
	array :=[...]int{1,2,3,4}
	for i :=0; i < len(array); i++ {
		fmt.Printf("%d 值: %d  \n", i, array[i])
	}

	// 使用 for range 循环
	array1 :=[...]string{"name","Victor","age"}
		for k,v :=range array1 {
			fmt.Printf("索引:%d ,值:%s \n", k,v)
		}

	// 不要索引
	for _,i :=range array1 {
		fmt.Printf("值:%s \n", i)
	}
}

在数组中寻找最大 or 最小值

package main

import (
	"fmt"
)

func main() {

	arr := [10]int{10, 4, 200, 43, 20, 324, 32, 90, 34, 31}

	// 找出最大值 or 最小值
	max := arr[0]
	for i := 1; i < len(arr); i++ {
		if max < arr[i] {
			max = arr[i]
		}
	}
	fmt.Println(max)
}
func main() {
    // 求出一个数组里面的最大值,并得到对应的下标
    // 1.声明一个数组
    // 2.假定第一个元素就是最大值,下标 0
    // 3.然后从第二个元素开始循环比较,如果发现有更大则替换
    var myArr [...]int = [...]int {30, -2, 3, 20, 15}
    max := myArr[0]
    maxIndex := 0
    for i := 1; i < len(myArr); i++{
        // 从第二个元素开始比较,如果有更大,则交换
        if max < myArr[i] {
            max = myArr[i]
            maxIndex = i
        }
    }
    // 打印结果
    fmt.Printf("max= %v ; maxIndex= %v", max, maxIndex)

}

数组的查找

package main

import (
	"fmt"
)


func main() {
	// 定义数组
	names := [4]string{"tom", "vic", "ccg", "vc"}

	var inName string
	fmt.Println("请输入要查找的名字=》")
	fmt.Scanln(&inName)

	// 顺序查找 第一种方式
	for i := 0; i < len(names); i++ {
		if inName == names[i] {
			fmt.Println("找到 :", inName, "下标:", i)
			break
		} else if i == (len(names) - 1){
			fmt.Println("没有找到 :", inName)
		}
	}

	// 顺序查找 第二种方式
	index := -1
	for i := 0; i < len(names); i++ {
		if inName == names[i] {
			index = i // 讲找到值的对应下标赋给 index
			break
		}
	}
	if index != -1 {
		fmt.Println("找到 :", inName, "下标:", index)
	} else {
		fmt.Println("没有找到 :", inName)
	}
}

冒泡排序

示例[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BhA5TfsZ-1678696518063)(images/image-20210331142932764.png)]

package main

import (
	"fmt"
)

func main() {
	arr := [10]int{10, 4, 200, 43, 20, 324, 32, 90, 34, 31}

	// 冒泡排序
	// 外层控制行
	for k := 0; k < len(arr)-1; k++ {
		// 内层控制列
		for j := 0; j < len(arr)-1-k; j++ {
			// 比较二个相邻元素,满足条件就交换数据
			// 升序排列使用大于号; 降序排列使用小于号
			if arr[j] > arr[j+1] {
				arr[j], arr[j+1] = arr[j+1], arr[j]
			}
		}
	}
	fmt.Println(arr)
}

数组作为函数参数和返回值

package main

import (
	"fmt"
)

// 数组作为函数参数和返回值
func bubbleSort(array [10]int) [10]int {
	for k := 0; k < len(array); k++ {
		for j := 0; j < len(array)-1-k; j++ {
			// 比较二个相邻元素,满足条件就交换数据
			if array[j] < array[j+1] {
				array[j], array[j+1] = array[j+1], array[j]
			}
		}
	}
	return array
}

func main() {

	arr := [10]int{10, 4, 200, 43, 20, 324, 32, 90, 34, 31}

	arr = bubbleSort(arr)
	fmt.Println(arr)
}

多维数组

多维数组的定义

二维数组
	var	数组名 [行数][列数] 数据类型

示例

package main

import "fmt"

func main() {
	// 多维数组
  // 定义了一个数组 A ,A 数组里面包涵了 2 个元素, 但是每个元素里面又包涵了 3 个元素
  A := [2][3]{
    {1, 3, 4},
    {34, 43, 54}
  }

  fmt.Println(A)
}

多维数组的遍历

package main

import (
	"fmt"
)

func main() {
	var arr = [3][3]int {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}

	// for 循环 来遍历
	for j := 0; j < len(arr); j++ {
		for k :=0; k < len(arr[j]); k++{
			fmt.Printf("%v \t", arr[j][k])
		}
		fmt.Println()
	}

	// for-range 来遍历, 使用 _ 省略索引的打印
	for _,x := range arr {
		for _,y := range x {
			fmt.Printf("%v \t", y)
		}
		fmt.Println()
	}
}

示例

fncu main() {
    // 创建一个 byte 类型的26元素的数组,分别放置 A-Z
    // 使用for 循环打印

    //使用for 循环,利用字符可以进行运算的特点来进行赋值 'A' +1 -> 'B'
    var myChars [26]byte
    for i := 0; i < len(26); i++{
        myChars[i] = 'A' + byte(i) // 需要将 i 装换成 byte 类型
    }
    for i := 0; i< 26; i++{
        // %c 该值对应的unicode码值
        fmt.Piintf("%c", myChars[i])
    }
}

示例

func main() {
    // 求数组的和及平均值
    // 1.定义数组
    // 2.求和
    // 3.求平均值

    var rSum [5]int = [...]int {20, -1, 10, 11}
    sum := 0
    for _, v := range rSum {
        // 累计求和
        sum += v
    }
    // 求平均值
    fmt.Printf("sum= %v ; 平均值= %v", sum, float64(sum) / float64(len(v)))
}

示例

package main

import (
	"time"
	"math/rand"
	"fmt"
)
 // 随机生成 五个随机数,并将其反转打印
 // 1.随机生成五个随机数,rand.Intn()
 // 2.存到数组
 // 3.反转打印
func main() {
	var intArr [5]int
	// 为了每次生成的随机数不一样,需要给一个seed值
	rand.Seed(time.Now().UnixNano())
	for i :=0; i < len(intArr); i++ {
		intArr[i] = rand.Intn(100)
	}
	fmt.Println(intArr)

	for x,y := 0, len(intArr) -1; x < y; x ,y = x + 1, y - 1{
		intArr[x], intArr[y] = intArr[y], intArr[x]
	}
	fmt.Println(intArr)
}

2、切片

切片是建立在数组之上的更方便、更灵活、更强大的数据结构,切片不存储任何元素而只是对现有数组的引用

切片对象非常小,是因为它是只有 3 个字段的数据结构:

  • 指向底层数组的指针

  • 切片的长度

  • 切片的容量

使用内置的 make 函数时,需要传入一个参数指定切片的长度,如:切片的长度是 6,这时候容量也是 6 ;当然也可以单独指定切片的容量

注意:容量必须 (cap) >= 长度(len) ,不能创建长度大于容量的切片

切片和数组的区别

定义数组必须写元素个数,而切片不需要写元素个数

// 数组
array := [4]int{1, 2, 3, 4}

// 切片
slice := []int{1, 2, 3, 4}

通过 make 创建切片

  • 通过 make 创建的切片可以指定切片的大小和容量

  • 如果没有给切片赋值,那么就会使用默认值(int、float => 0,string => “”,bool => false)

  • 通过make创建的切片对应的数组是由make底层维护,对外不可见,只能通过 slice访问各个元素

基本语法

var  切片名 []type = make([], len, [cap])

// type 数据类型
// len  大小,即长度
// cap  容量,(容量必须 >= 长度)

 // 没有赋值,默认都是 0
var slice []int = make([]int, 5, 10)

// 简写
slice := make([]int, 5, 10)
slice := make([]int, 5)

// 赋值
slice[2] = 5

切片直接引用数组

// 从下标 startIndex 开始,取到下标 endIndex, 但是数据不包含 arr[endIndex]
var slice = arr[startIndex:endIndex]

var slice = arr[0:end]   // 简写 var slice = arr[:end]

var slice = arr[start:len(arr)]  // 简写 var slice = arr[start:]

var slice = arr[0:len(arr)]   // 简写 var slice = arr[:]

	str := []string{"北京", "上海", "广州", "深圳"}
	fmt.Println(str[0:])	// 取所有值
	fmt.Println(str[0:3])	// 取0 、1、2 的值
	fmt.Println(str[:len(str)]) // 取所有值

切片简单操作示例

// 声明一个切片并追加数据, 使用内置函数 append
var vic []string
vic = append(vic, "Go")

// 同时声明并初始化一个切片
v := []string{"GO", "JAVA"}
fmt.Println(v)

// 切片长度是可变的;使用内置函数 make 创建长度不为 0 的切片
s := make([]int, 5)
fmt.Println(s)


// 取值和赋值,和数组一样
s[1] = 111
s[3] = 333
fmt.Println(s)
fmt.Println(s[4])

// 使用内置函数 len 获取切片的长度
fmt.Println(len(s))

// append 函数不会改变原切片,而是生成了一个新的切片,需要用原来的切片接收这个新切片
s = append(s, 222, 444)

// 把一个切片追加到另一个切片, 通过 ... 操作符
x := []int{1, 2, 3, 4}
y := []int{11, 22, 33}
x = append(x ,y...)

// 切片也支持从一个切片拷贝元素到另一个切片,使用内置函数 copy
k := []string{"a", "b"}
j := []string{"x", "y"}
copy(k, j) // 拷贝切片 j 中的元素到 k 中
fmt.Println(k)

切片的迭代

// 切片是一个集合,可以使用 for range 循环迭代
S1 := []string{"Go", "JAVA", "Python"}

for k,v := range S1{
  fmt.Printf("索引 %d => 值 %s \n", k, v)
}

// 如果不打印索引,可以使用 _ 来忽略
for _,v := range S1{
  fmt.Printf("值 %s \n",  v)
}

// 使用传统 for 循环,配合内置函数 len
for i :=0; i < len(S1); i++{
  fmt.Println("值:", S1[i])
}

多维切片

// 同数组一样,切片也可以有多个维度,并且可以不固定长度
S2 := [][]string{
  {"C", "JAVA"},
  {"Go", "Perl"}
}

for _,v := range S2 {
  fmt.Println(v)
}

切片作为函数参数

package main

import "fmt"

func test(v1 []string) {
	fmt.Println(v1[1])
  fmt.Printf("%v \n", v1)
}

func main() {
	v1 := []string{"Go", "Java"}
	test(v1)
}

切片作为函数参数和返回值

package main

import "fmt"

func test(v1 []int) (v2 []int) {
	v2 = v1[0:2]
	fmt.Printf("v2数据: %v \n", v2)
	return
}

func main() {
	v1 := []int{11, 22, 33, 44}
	fmt.Println("v1数据:", v1)

	test(v1)

}

3、Map
  • Map 是 Go 语言中的内置类型,它将 键与值 绑定到一起,可以通过键获取相应的值
  • Map 类似 Python 中字典的概念,是一种无序的键值对集合
  • 可以通过 Key 来快速检索数据,key 类似索引,指向数据的值
  • map 的长度是自动扩容的
  • map 中的数据是无序存储的
  • 在 map 中键不允许重复必须是唯一的,值可以重复

基本语法

// map 赋值方式,必须先声明、再初始化,最后赋值
var Victor map[string]string
Victor = make(map[string]string)
Victor["name"] = "Tom"


// 或者
M := make(map[string]int)
M["age"] = 20
M = map[string]int{
  "cc": 10,
  "vv": 20,
}

// 直接初始化批量并赋值
M1 := map[string]int{
  "age": 20,
  "sex": 18
}

// 指定 map 长度,一般直接省略
M2 := make(map[string]string, 10)

map 增改删

// 向 map 中插入数据,语法和数组类似

shop := make(map[string]int)
shop["Apple"] = 20
shop["Orange"] = 15
shop["Banana"] = 9

// 修改数据
shop["Apple"] = 30


// 删除数据,使用内置函数 delete
// delete(map, key) 用于删除 map 中的 key,delete 函数没有返回值
delete(shop, "Banana")

遍历 map

shop := map[string]int{
  "Apple": 30,
  "Banana": 15,
}

// 使用 range 循环
for k,v := range shop{
  fmt.Println(k,v)
}

// 判断 key 是否存在
if v, ok := shop["Apple"]; ok{
  fmt.Println("Apple:", v)
}else {
  fmt.Println("Key is Not found")
}

map 作为函数参数

func test(m map[int]string){
  m[101] = "孙悟空"
  fmt.Println(m)
}

func main() {
  People := map[int]string{
    101: "关羽",
    102: "张飞",
    103: "刘备",
  }
  fmt.Println(People)

  test(People)

}

map 值排序

shop := map[30]string{
  30: "tea",
  15: "coffee",
  50:"sweet",
}

var keys []int
for k,_ := range shop{
  keys = append(keys, k)
}
fmt.Println(keys)
sort.Ints(keys[:])
fmt.Println(keys)
for _,v := keys{
  fmt.Println(v, shop["v"])
}

统计单词出现的次数

s := "how to contribute who to contact about how"

// 定义 map 存放单词
wordCount := make(map[string]int)

// 分割字符串中的单词
words := strings.Split(s, " ")

// 遍历统计单词
for _, w := range words{
  if v, ok := wordCount[w]; ok {
    // map 中有这个单词的统计出现就 +1
    wordCount[w] = v + 1
  }else {
    // map 中没有这个单词的统计记录就等于 1
    wordCount[w] = 1
  }
}


// 打印单词出现的次数
for k,v := range wordCount {
  fmt.Println(k,v)
}

双层嵌套

// 存放学生信息,学生 name、age
	studentMap := make(map[string]map[string]string)

	// 添加数据
	studentMap["stu1"] = make(map[string]string)
	studentMap["stu1"]["Name"] = "vic"
	studentMap["stu1"]["Age"] = "20"

	studentMap["stu2"] = make(map[string]string)
	studentMap["stu2"]["Name"] = "Tom"
	studentMap["stu2"]["Age"] = "18"

	fmt.Println(studentMap)
	fmt.Println(studentMap["stu1"])
	fmt.Println(studentMap["stu1"]["Name"])


// 定义字典,key 再定义字典 key,键值为切片
	City := make(map[string]map[string][]string)
	// 如果插新加入的元素也是个 map 的话需要再次 make()
	Area := make(map[string][]string)

	Area["朝阳区"] = []string{"工业大学", "语言大学", "传媒大学"}
	Area["海淀区"] = []string{"清华大学", "理工大学", "农业大学"}

	City["北京"] = Area

	for k, v := range Area {
		fmt.Println(k, v)
		for x, y := range v {
			fmt.Println(x, y)
		}
	}

三层嵌套

//定义国家
	Country := make(map[string]map[string]map[string][]string)
	//定义城市
	City := make(map[string]map[string][]string)
	//定义区
	Area1 := make(map[string][]string)
	Area2 := make(map[string][]string)
	//赋值
	Area1["朝阳区"] = []string{"工业大学", "语言大学", "传媒大学",}
	Area1["海淀区"] = []string{"清华大学", "理工大学", "农业大学",}

	Area2["黄浦区"] = []string{"复旦大学", "同济大学"}
	Area2["静安区"] = []string{"交通大学", "财经大学"}

	City["北京"] = Area1
	City["上海"] = Area2

	Country["中国"] = City

	for k, v :=range City{
		fmt.Println(k, v)
		for x, y :=range v {
			fmt.Println(x, y)
			for i := range y {
				fmt.Println(y[i])
			}
		}
	}
4、结构体

Struct(结构体) 可以声明一个新的类型,作为其他类型的属性或者字段

示例:

创建一个自定义类型 Person 代表一个人的实体,实体有用属性:姓名和年龄。这样的类型就称之为 struct

  • Name 字段 string类型,用来保存用户名属性
  • Age字段int 类型,用来保存 年龄属性
type Person struct{
  Name string
  Age int
}

基本语法声明类型

// 1. 按照顺序提供初始化
P := Person{"Vic", 20}

// 2. 通过 feild:value 的方式初始化赋值
P := Person{Name: "Vic", Age: 20}

// 3. 先定义 Person 类型的变量再赋值
var P Person
P.Name = "Vic"
P.Age = 20

// 4. 通过 new 函数分配一个指针,此次的 P 类型为 *Person
P := new(Person)

  在结构体中使用 ==  或者 !=  可以对二个结构体的成员进行比较

简单示例

package main

import (
	"fmt"
)

// 声明一个结构体
type Person struct {
	Name string
	Age  int
}

// 比较二个人的年龄,返回年龄大的那个人,并且返回年龄差
// 定义函数,struct 传值
func Older(p1, p2 Person) (Person, int) {
	if p1.Age > p2.Age {
		return p1, p1.Age - p2.Age
	}
	return p2, p2.Age - p1.Age
}

func main() {
	// 定义变量,类型为 struct
	var T1 Person
	T1.Name, T1.Age = "Tom", 20

	// 对应字段初始化值
	T2 := Person{Name: "Vic", Age: 30}

	// 按照 struct 字段定义的顺序进行赋值
	T3 := Person{"Bob", 10}

	T1_T2_Older, T1_T2_Diff := Older(T1, T2)
	T1_T3_Older, T1_T3_Diff := Older(T1, T3)
	T2_T3_Older, T2_T3_Diff := Older(T2, T3)

	fmt.Printf("%s 和 %s 对比, %s 的年龄最大,二人相差:%d \n", T1.Name, T2.Name, T1_T2_Older.Name, T1_T2_Diff)
	fmt.Printf("%s 和 %s 对比, %s 的年龄最大,二人相差:%d \n", T1.Name, T3.Name, T1_T3_Older.Name, T1_T3_Diff)
	fmt.Printf("%s 和 %s 对比, %s 的年龄最大,二人相差:%d \n", T2.Name, T3.Name, T2_T3_Older.Name, T2_T3_Diff)
}

匿名结构体

如果结构体是临时使用,可以不用起名字,直接使用

var dog struct{
  id int
  name string
}
dog.id = 1
dog.name = "小黑"
fmt.Println(dog)

匿名字段

struct 定义的时候字段名与其类型是一一对应的,实际上 GO 支持只提供类型而不写字段名的方式;也就是匿名字段

这是一种可以把已有的类型声明在新的类型中的一种方式,对于代码的复用非常重要。

在 Go 的标准库里经常有这种组合,比如 io 包里,可以看到 ReadWrite 接口就是嵌入 ReaderWriter 接口而组合成新的接口。

示例

创建Human 就是匿名字段(内部字段),Student 是外部字段, 外部字段可以添加自己的方法、字段属性

  • 对应内部类型的属性和方法,可以用外部类型直接访问,也可以通过内部类型访问
  • 外部类型新增的方法属性字段,只能外部类型访问,因为内部类型没有这项

当匿名字段是struct的时候,那么这个struct 所拥有的全部字段都被隐式低引入了当前定义的这个struct

package main

import (
	"fmt"
)

type Human struct{
  Name string
  Age int
  Wight int
}

type Student struct{
  Human  // 匿名字段。 默认Student包涵Human的所有字段,通过匿名字段实现继承操作
  Level string
}

func main() {
  // 初始化
  T := Student{Human{"Tom", 20, 90}, "CTO"}

  // 访问相应字段
  fmt.Println("Name: ", T.Name)
  fmt.Println("Age: ", T.Age)
  fmt.Println("Level: ", T.Level)

  // 修改 Age
  T.Age = 30
  fmt.Println("Change Age: ", T.Age)

  // 修改 Level
  T.Level = "CEO"
  fmt.Println("Change Level: ", T.Level)
}

外部类型也可以声明同名的字段或者方法,来覆盖内部类型

内部类型 Human 有一个 Age 方法,外部进行覆盖,同名重写Age,不管我们如何同名覆盖,都不会影响内部类型,可以通过访问内部类型来访问它的方法、属性等字段。

最外层的优先访问,当通过Student.Age 访问的时候,访问的是Student里面的字段,而不是Human里面的字段,访问内部字段就需要使用Student.Human.Age

package main

import (
	"fmt"
)

type human struct {
	Name  string
	Age   int // human 拥有的 Age 字段
	Wight int
}

type student struct {
	human // 匿名字段。 默认Student 包涵 Human 的所有字段
	Level string
	Age   int // student 拥有同名的 Age 字段
}

func main() {
	// 初始化
	T := student{human{"Tom", 20, 90}, "CTO", 30}

	// 访问 student 的 Age
	fmt.Println("student Age: ", T.Age)

	// 访问 human 的 Age
	fmt.Println("human Age: ", T.human.Age)
}

多重继承

package main

import "fmt"

type testA struct {
	id   int
	name string
}

type testB struct {
	testA
	age int
}

type testC struct {
	testB
	score int
}

func main() {
	// 定义机构体变量
	var s testC
	s.id = 001
	s.name = "Vic"
	s.age = 20
	s.score = 90

	fmt.Println(s)
	fmt.Println(s.testA.name)
}

struct 工厂模式

解决小写字母不能被调用问题

目录结构

─➤ tree
.
├── main.go
└── model
└── student.go

student.go 文件

package model

//  定义一个学生结构体
type student struct {
	Name string
	age  int
}

// 因为定义的 student 结构体首字母为小写,所以只能在 model 使用
// 使用 工厂模式来解决外部不能访问的问题
func GetStudent(n string, a int) *student {
	return &student{
		Name: n,
		age:  a,
	}
}

// 因为 age 字母是小写,所以在其他包不可以直接访问,因此提供一个方法
func (s *student) GetAge() int {
	return s.age
}

main.go 文件

package main

import (
	"example/day1/005/model"
	"fmt"
)

func main() {
	// 初始化
	T := model.GetStudent("Vic", 20)

	// 访问 student 结构体
	fmt.Printf("student struct: %+v \n", *T)

	// 访问 student 的 age
	fmt.Println("student age: ", T.GetAge())
}

类型为结构体的数组

  • 数组定义后可以增加数据
package main

import "fmt"

type student struct {
	id   int
	name string
	age  int
}

func main() {
	// 定义一个数组,数据类型为 student
	var arr [4]student

	arr[0] = student{000, "Vic", 18}
	arr[1] = student{001, "Tom", 8}
	arr[2] = student{002, "Bob", 20}
	arr[3] = student{003, "Sky", 15}

	fmt.Println(arr)

	// 按照年龄排序结构体数组中的数据
	for k := 0; k < len(arr)-1; k++ {
		for j := 0; j < len(arr)-1-k; j++ {
			// 比较结构体成员 年龄
			if arr[j].age > arr[j+1].age {
				// 结构体数组中的元素 允许相互赋值 ,将结构体成员中的数据进行互换
				arr[j], arr[j+1] = arr[j+1], arr[j]
			}
		}
	}
	for i := 0; i < len(arr); i++ {
		fmt.Println(arr[i])
	}
}

类型为结构体的切片

  • 切片定义后可以增加数据
package main

import "fmt"

type student struct {
	id   int
	name string
	age  int
}

func main() {
	// 定义一个切片,数据类型为 student
	var arr []student
	arr = []student{
		{100, "张飞", 20},
		{101, "关羽", 25},
		{102, "刘备", 30}}

	fmt.Println(arr)

	// 切片中增加数据
	arr = append(arr,
		student{103, "嫦娥", 16},
		student{104, "貂蝉", 18},
	)
	fmt.Println(arr)
}

类型为结构体的map

package main

import "fmt"

type student struct {
	name string
	age  int
}

func main() {
	// 定义一个 map,数据类型为 student, 一对一
	var M1 = make(map[int]student)
	M1[101] = student{"张飞", 20}
	M1[102] = student{"关羽", 25}
	M1[103] = student{"刘备", 30}

	fmt.Println(M1)

  
	// 定义一个 map,数据类型为 []student, 一对多
	var M2 = make(map[int][]student)
	// 因为类型是切片,所以要使用 append 增加数据
	M2[101] = append(M2[101], student{"曹操", 20}, student{"周瑜", 20})
	M2[102] = append(M2[102], student{"张飞", 20}, student{"黄忠", 20})

	fmt.Println(M2)
	for k, v := range M2 {
		for j, l := range v {
			fmt.Println("key: ", k, "index: ", j, "Value:", l)
		}
	}
}

类型为结构体的函数、返回值

package main

import "fmt"

type stu struct {
	name string
	age  int
}

// 结构体作为函数参数
func test1(s stu) {
	fmt.Println(s.name)
	fmt.Println(s.age)
}

// 结构体作为函数参数、返回值
func test2(s stu) []stu {
	stus := []stu{}
	stus = append(stus, s)
	stus = append(stus, stu{"Tom", 18})

	return stus
}

func main() {
	S1 := stu{"Vic", 20}
	test1(S1)

	fmt.Println(test2(S1))

}
5、指针

什么是指针

  • 一个指针变量指向了一个值的内存地址
  • 类似变量和常量,在使用指针前需要声明指针
  • 获取变量的地址用 &, 例如获取var ret string 的地址&ret
  • 获取指针类型所指向的值使用 *, 例如获取var ret *string 指针变量的值使用*ret获取
  • 所有类型都有对应的指针类型*int*string等等
// 指针声明
var var_name *var-type

var_name: 指针变量名
var-type: 指针类型
**号用于指定变量是作为一个指针


// 指向整形
var ptr *int   

// 指向指针的指针变量值需要使用二个 * 号
var pptr **int

V = 200
// 指针 ptr 地址
ptr = &a

//指向指针 ptr 地址
pptr = &ptr

// 使用指针访问值
fmt.Printf("指针变量 *ptr = %d\n", *ptr )
fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)

创建指针的另一种方法 new() 操作

package main

import(
	"fmt"
)

func main(){
  var p *int // nil  定义空指针
  fmt.Println(p)
  
  // 为指针变量创建一块内存空间
  // 在堆区创建空间
  p1 = new(int) // new 创建好的空间值为数据类型的默认值 0
  
  // 打印 p1 的值
  fmt.Println(p1)
  
  // 打印 p 指向空间的值
  fmt.Println(*p1)
  
  *p1 = 10
  fmt.Println(*p1)
  
  *p1 = 20
  fmt.Println(*p1)
  
}

当传递一个参数值到被调用函数里是,实际上是copy了这个值进行传递,当在被调用的函数里修改这个值,只是修改了copy过的这个值, 原有的这个值是不会发生变化的

package main

import "fmt"

// 实现了 参数 + 1 的函数
func add(a int) int {
	a = a + 1 // 改变了 a 的值
	return a  // 返回一个新的值
}

func main() {
	x := 2
	fmt.Println("x+1 : ", add(x)) // 调用 add() 函数,返回新值
	fmt.Println("x : ", x)        // 还是返回原有的值
}

上面的操作,虽然调用了 add 函数,并在函数中执行了a=a+1操作,但是x变量的值并没有发生变化,因为调用add 函数的时候,接收的参数其实是Xcopy值,并不是x本身

如果真的需要改变x变量本身,就需要用到指针操作,需要add函数知道x变量所在地址,才能修改x变量的值。

x地址&x传递给函数,将函数的参数类型改为*int,指针类型,这样就能在函数中修改X变量的值了,此时参数仍然是copy值,参数copy的是一个指针

& 符号,获取变量的地址

  • 符号,获取变量的值
package main

import "fmt"

// 实现了 参数 + 1 的函数,参数类型改成了 指针
func add(a *int) int {
	*a = *a + 1 // 使用指针方式改变了 a 的值
	return *a   // 返回一个新的值
}

func main() {
	x := 2
	fmt.Println("x+1 : ", add(&x)) // 调用 add() 函数,使用&获取值
	fmt.Println("x : ", x)         // 使用指针传值,原有变量值已经被更改
}

指针作为函数参数

  • Go 语言允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可
package main

import "fmt"

// 指针作为函数参数
func swap(a *int, b *int) {
	// 实现 a 、b 值交互
	*a, *b = *b, *a
}

func main() {
  // 定义局部变量 
	a, b := 10, 20
	fmt.Println("修改之前:", a, b)

	// 调用函数用于交换值, 指针作为函数参数,必须是地址传值
	swap(&a, &b)
	fmt.Println("修改之后:", a, b)

}

数组指针

前面用数组作为函数参数,但是数组作为参数进行传递是值传递,如果想引用,可以使用数组指针

定义一个数组

package main

import "fmt"

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	fmt.Println("修改之前:", a)

	// 数组指针是让一个指针指向数组,然后通过操作该指针来操作该数组
	// 定义指针指向数组
	var p *[5]int

	// 将指针与数组建立关系
	p = &a
	fmt.Println("修改之后:", *p)

	// 通过指针操作数组
	p[0] = 10
	fmt.Println("修改之后:", *p)
	fmt.Println("修改之前:", a)

	// 循环打印数组指针中的数据
	for index, value := range *p {
		fmt.Printf("index: %d ; value: %d \n", index, value)
	}
}

这时指针 p , 指向了数组 a,对指针 p 的操作实际就是对数组 a 的操作,所以如果执行打印 *p,会输出数组a中的值,也可以通过 *p 结合下标将对应的值取出来进行修改,最终在main 函数中输出数组 a,发现其原始也已经修改

指针数组

  • 数组指针是让一个指针指向数组,然后通过操作该指针来操作数组,

  • 指针数组指的是一个数组中存储的都是指针(也就是地址),也就是一个存储了地址的数组

// 指针数组定义
var p [2]*int
var i int = 10
var j int = 20

// 将地址赋值给指针数组
p[0] = &i
p[1] = &j

// 打印查看
fmt.Println(p[0])
fmt.Println(p[1])

 注意:指针数组的定义方式 与 数组指针的定义方式不一 ; 指针数组是将 “*” 放在了下标的后面

切片指针

切片指针作为函数参数是地址,形参可以改变实参的值

package main

import "fmt"

func test(s []int) {
	s = append(s, 4, 5, 6)
}

// 使用切片指针作为函数参数,可以改变原切片的值
func test1(s *[]int) {
	*s = append(*s, 4, 5, 6)
}
func main() {
	s := []int{1, 2, 3}
	fmt.Println(s)
	test(s)
	fmt.Println(s)

	// 切片指针作为函数参数是地址,形参可以改变实参的值
	test1(&s)
	fmt.Println(s)
}


指针切片

package main

import (
	"fmt"
)

func main() {
	// 指针切片
	var p []*int

	a := 10
	b := 20
	c := 30
	p = append(p, &a, &b)

	p = append(p, &c)
	fmt.Println(p)

	for k, v := range p {
		fmt.Println(k, *v)
	}
}

结构体指针

package main

import "fmt"

type people struct {
	name string
	age  int
}

func main() {
	// 定义结构体变量并赋值
	P := people{"Vic", 20}
	fmt.Println(P)

	// 定义结构体指针,指向变量的地址
	var ptr *people

	// 结构体指针指向变量的地址
	ptr = &P
	fmt.Println(*ptr)

	// 通过结构体指针间接操作结构体成员信息
	// (*ptr).name = "Skr"
	// 通过指针可以直接操作结构体成员
	ptr.name = "Skr"
	fmt.Println(P)
}

结构体切片

package main

import "fmt"

type people struct {
	name string
	age  int
}

func main() {
	// 定义结构体切片
	var p []people

	// 结构体切片指针
	ptr := &p

	fmt.Printf("%T \n", ptr)

	*ptr = append(*ptr, people{"小猪佩奇", 20})
	*ptr = append(*ptr, people{"乔治", 30})

	fmt.Println(p)

	for i := 0; i < len(*ptr); i++ {
		fmt.Println((*ptr)[i])
	}
}

八、面向对象

1、方法的定义和使用

由于在Go中没有class的关键字,也就是其他语言经常在面向对象使用的方法,但是Go通过

struct method 方法组合来实现面向对象

go中的方法是一种特殊的函数,定义与 结构体struct之上(与struct关联、绑定),被称为struct的接收者receiver, 方法就是有接收者的函数

方法的语法

type myName struct{}

func(recv myName) my_method(args) return_type{}
func(recv *myName) my_method(args) return_type{}

- 参数说明
- myName : 定义的结构体
- recv : 接收该方法的结构体(recevier)
- my_method : 方法名称
- args : 参数
- return_type : 返回的值类型
## 从语法上可以看出,一个方法和一个函数非常相似,只是多了一个接收类型

方法的声明

package main

import "fmt"

// 定义结构体
type testA struct {
	name string
}

// 为获取 name 属性定义一个方法
func (t *testA) GetName() string {
	return t.name
}

func main() {
	// 定义结构体变量
	T := testA{"乔峰"}

	// 调用方法
	fmt.Println(T.GetName())
}


// 对上面语法的解析说明
1func (t *testA) GetName() string {}  表示 testA 结构体有一个方法,方法名为 GetName
2(t *testA) : 表示 GetName 方法是和 testA 类型绑定的

method方法

method 是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在func后面增加了receiver(也就是method所依从的主体)

func (r ReceiverType) funcName(Parameters) (results)
			接收者							函数名								正常的函数结构
  • 虽然 method 的函数名字一模一样,但是如果接收者的类型不一样,那么method 就不一样
  • method 里面可以访问接收者的字段
  • 通过 method 通过. 访问,就像 struct里面访问字段一样

类型别名和方法的结合

package main

import "fmt"

// 面向对象 , 方法: 给某个类型绑定一个函数
// 给类型定义一个别名
type vic int

// V 叫接收者,接收者就是传递的一个参数
func (V vic) add(b vic) vic {
	return V + b
}

func main() {
	// 定义一个变量,作为传递参数
	var k vic = 10

	// 调用方法: 变量名.函数(所需参数)
	v := k.add(100)

	fmt.Println(v)

}

方法示例

定义 method方法,计算长方形 和 圆 的面积

package main

import (
	"fmt"
	"math"
)

// 定义结构体长方形的  长、宽
type Rectangle struct {
	width, height float64
}

// 定义圆的半径
type Circle struct {
	redius float64
}

// 定义 method 方法,计算长方形 、 圆的面积
// 不同 struct 的 method 方法不同
func (r Rectangle) getArea() float64 {
	return r.height * r.width
}

func (c Circle) getArea() float64 {
	return c.redius * c.redius * math.Pi
}

func main() {
	// 长方形传值
	r1 := Rectangle{10, 20}

	// 圆传值
	c1 := Circle{35}

	// 调用方法
	fmt.Println("r1: ", r1.getArea())
	fmt.Println("c1: ", c1.getArea())
}

上面的示例中,getArea()分别属于RectangleCircle ,于是他们的 Receiver 就变成了RectangleCircle

ReceiverType也可以是指针, 二者的差别在于,

  • 指针为Receiver会对实例对象的内容发生操作
  • 普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作
package main

import "fmt"

type testA struct {
	name string
}

type testB struct {
	name string
}

// 取一个变量 a, a 就是接收者,接收者类型就是 struct testA ,getPrint 就是方法名, 参数在 getPrint() 的括号中定义
// a 正常传递
func (a testA) getPrint() {
	a.name = "AA"
	fmt.Println("A")
}

// 加上 * 代表指针传递
func (b *testB) getPrint() {
	b.name = "BB"
	fmt.Println("B")
}

func main() {
	// 值类型不使用指针,在这个方法结束之后,值不会被修改
	a1 := testA{}
	a1.getPrint()
	fmt.Println(a1.name)

	// 使用指针,在这个方法结束之后,值会被修改
	b1 := testB{}
	b1.getPrint()
	fmt.Println(b1.name)
}

上面的例子可以看出

  • 如果一个 methodreceiver*T ,可以在一个 T类型的实例变量V上面调用这个 method,而不需要 &V 去调用这个 method
  • 如果一个methodreceiverT ,可以在一个 T类型的变量 P 上面调用这个method,而不需要 P 去调用这个 method

method 方法继承

method方法也是可以继承的,如果匿名字段实现了一个 method方法 ,那么包涵这个匿名字段的 也能调用该method方法

package main

import "fmt"

// 定义结构体 people
type people struct {
	name string
	age  int
}

// 定义结构体 student 包涵 people
type student struct {
	people // 匿名字段
	school string
}

// 定义结构体 employee 包涵 people
type employee struct {
	people  // 匿名字段
	company string
}

// 定义方法 people上面定义方法
func (p *people) sayHi() {
	fmt.Printf("Hi, I'm %s, age %d .\n", p.name, p.age)
}
func main() {
	//赋值
	tom := student{people{"Tom", 20}, "北京大学"}
	vic := employee{people{"Vic", 28}, "CTO"}

	tom.sayHi()
	vic.sayHi()

}

method 方法重写

上面的例子,如果employee 想实现自己的sayHi,可以在employee 定义一个方法,重写匿名字段的方法

package main

import "fmt"

// 定义结构体 people
type people struct {
	name string
	age  int
}

// 定义结构体 student 包涵 people
type student struct {
	people // 匿名字段
	school string
}

// 定义结构体 employee 包涵 people
type employee struct {
	people  // 匿名字段
	company string
}

// 定义方法 people上面定义方法
func (p *people) sayHi() {
	fmt.Printf("Hi, I'm %s, age %d .\n", p.name, p.age)
}

// 定义 employee 的方法重写 people 的方法
func (e *employee) sayHi() {
	fmt.Printf("Hi, I'm %s, my work %s, age %d .\n", e.name, e.company, e.age)
}
func main() {
	//赋值
	tom := &student{people{"Tom", 20}, "北京大学"}
	vic := employee{people{"Vic", 28}, "CTO"}

	tom.sayHi()
	vic.sayHi()

}
2、接口的定义和使用

接口的声明

接口是一种约定,是一个抽象的类型,它只有一组接口方法,我们并不知道它内部的实现;

虽然我们不知道接口是什么,但是我们可以知道利用它提供的方法做什么。

interface 类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口

注意:

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

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

  • 接口中不能有任何变量

  • 一个接口(A 接口)可以继承多个别的接口(B、C 接口),此时要实现A接口,也必须将B、C接口的方法也全部实现

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

  • 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口

示例

package main

import "fmt"

// 1.定义接口
type usb interface {
	// 声明方法列表
	Read()
	Write()
}

// 2. 创建对象
type mobile struct {
	name  string
	speed int
}

type computer struct {
	name  string
	speed int
}

// 实现方法
func (m *mobile) Read() {
	fmt.Printf("%s 正在读取数据速度为:%d M\n", m.name, m.speed)
}

func (m *mobile) Write() {
	fmt.Printf("%s 正在写入取数据速度为:%d M\n", m.name, m.speed)
}

func (c *computer) Read() {
	fmt.Printf("%s 正在读取数据速度为:%d M\n", c.name, c.speed)
}

func (c *computer) Write() {
	fmt.Printf("%s 正在写入取数据速度为:%d M\n", c.name, c.speed)
}

func main() {
	m := mobile{"华为荣耀", 20}
	// 定义接口变量 u
	var u usb
	// u 变量能存储 moblie 信息
	u = &m
	u.Read()
	u.Write()

	// u 变量也能能存储 computer 信息
	c := computer{"MacBook", 100}
	u = &c
	u.Read()
	u.Write()

}

接口的继承

package main

import "fmt"

// 1.定义接口 子集
type humaner interface {
	Sayhi()
}

// 超集
type personer interface {
	humaner // 子集
	Sing(string)
}

// 2. 定义对象
type student struct {
	name string
	age  int
	sex  string
}

// 3.定义方法
func (s *student) Sayhi() {
	fmt.Printf("大家好,我叫 %s, 今年 %d 岁了,是 %s 生。\n", s.name, s.age, s.sex)
}

func (s *student) Sing(name string) {
	fmt.Printf("大家好,我叫 %s,给大家唱首歌 %s\n", s.name, name)
}

func main() {
	var h humaner
	h = &student{"王菲", 18, "女"}
	h.Sayhi()

	var p personer
	p = &student{"林忆莲", 30, "女"}
	// 继承子集的方法
	p.Sayhi()
	p.Sing("霸王别姬")
}

接口实现多态

多态:同一件事情由于条件不同而产生的结果不同;

由于Go语言的结构体不能体现相互转换,所以没有结构体的多态;

只有基于接口的多态

package main

import "fmt"

// 1. 定义接口
type humaner interface {
	sayHello()
}

// 3.定义结构体对象
type person struct {
	name string
	sex  string
	age  int
}
type student struct {
	person
	score int
}
type teacher struct {
	person
	subject string
}

// 3.定义方法
func (s *student) sayHello() {
	fmt.Printf("我叫 %s, 是一名学生, 今年 %d 岁了,是 %s 生, 成绩是 %d\n", s.name, s.age, s.sex, s.score)
}
func (t *teacher) sayHello() {
	fmt.Printf("我叫 %s, 是一名教师, 今年 %d 岁了,是 %s 生, 学科是 %s\n", t.name, t.age, t.sex, t.subject)
}

// 4. 实现多态
// 多态是将接口类型作为函数参数, 多态实现了接口的统一处理
func sayHello(h humaner) {
	h.sayHello()
}

func main() {
	var h humaner
	h = &student{person{"郭靖", "男", 16}, 99} //接口不能实例化,只能对接口的结构体实例化
	sayHello(h)                             //多态,条件不同结果不同

	h = &teacher{person{"欧阳锋", "男", 36}, "毒师"} //接口不能实例化,只能对接口的结构体实例化
	sayHello(h)                                //多态,条件不同结果不同
}

空接口的定义和使用

interface(interface{}) 不包含任何的方法,正因为如此,所有的类型都实现了空接口,

interface在我们需要存储任意类型数值的时候特别有用,因为它可以存储任意类型的数值

package main

import (
	"fmt"
)

// 如果一个函数把 interface{} 作为参数,那么它可以接受任意类型的值作为参数
// 如果一个函数返回值 interface{},那么也就可以返回任意类型的值
func main() {
	// 定义 空接口 a
	var a interface{}
	// a 可以存储任意类型的值
	a = 100
	fmt.Println(a)

	a = "Hi"
	fmt.Println(a)

	// 空接口切片
	var k []interface{}
	k = append(k, "21", "乔峰", "vic")

	for v := 0; v < len(k); v++ {
		fmt.Println(k[v])
	}

	for _, i := range k {
		fmt.Println(i)
	}
}

3、类型断言

在Go语言中可以使用 对象.(指定的类型) 判断该对象是否是指定的类型

package main

import "fmt"

func main() {
	// 定义空接口切片并初始化, 通过 make 创建的切片可以指定切片的大小
	arr := make([]interface{}, 4)
	arr[0] = 123
	arr[1] = 12.3
	arr[2] = "小龙女"
	arr[3] = []int{1, 2, 3}

	// 对切片进行类型断言
	for _, k := range arr {
		if data, ok := k.(int); ok {
			fmt.Println("整型数据", data)
		} else if data, ok := k.(float64); ok {
			fmt.Println("浮点型数据", data)
		} else if data, ok := k.(string); ok {
			fmt.Println("字符串数据", data)
		} else if data, ok := k.([]int); ok {
			fmt.Println("切片数据", data)
		}
	}

}

九、异常处理

所谓异常:就是当Go检测到一个错误时,程序就无法正常执行了,反而出现了一些所谓错误的提示,这就是所谓的异常。所以为了保证程序的健壮性,要对异常的信息进行处理。

1、error 接口

使用error 接口返回错误信息

package main

import (
	"errors"
	"fmt"
)

// 编辑时异常
// 编译时异常
// 运行时异常
func test(a, b int) (ret int, err error) {
	// 0 不能作为除数
	if b == 0 {
		err = errors.New("0 不能作为除数")
		return
	} else {
		ret = a / b
		return
	}
}

func main() {
	// 赋值
	ret, err := test(10, 0)

	// err 不等于空表示有错误信息
	if err != nil {
		fmt.Println(err.Error())
	} else {
		fmt.Println(ret)
	}
}
2、panic 函数

panic 函数返回的是让程序崩溃的错误

一般而言,当panic 异常发生时,程序会中断运行,随后程序崩溃并输出日志信息

package main

import (
	"fmt"
)

// 编辑时异常
// 编译时异常
// 运行时异常
func test(a, b int) (ret int) {
	ret = a / b
	return
}

func main() {
	// 赋值
	ret := test(10, 0)

	fmt.Println(ret)

}

直接调用panci使程序崩溃终止运行

package main

import (
	"fmt"
)

func t1() {
	fmt.Println("T1")
}

func t2() {
	// fmt.Println("T2")
	// 可以在程序中直接调用 panic,调用后程序会终止运行
	panic("T2")
}

func t3() {
	fmt.Println("T3")
}

func main() {
	t1()
	t2()
	t3()

}

3、延迟调用 defer
  • defer 用于注册延迟调用

  • 这些调用直到return前才被执行

  • 多个 defer 语句,按照先进后出的方式执行

  • defer 语句中的变量,在defer声明时就决定了

用途

  • 关闭文件句柄
  • 锁资源释放
  • 数据库链接释放
package main

import (
	"fmt"
)

func TryDefer() {
	defer fmt.Println(1)
	defer fmt.Println(2)
	fmt.Println(3)	
}

func main() {
	TryDefer()
}
4、recover

运行时panic异常一旦被引用就会导致程序崩溃,这不是我们愿意看到的,Go语言提供了专用于拦截运行时 panic 的内建函数 recover,它可以是当前程序从运行时 panic 的状态中恢复并重新获得流程控制权

注意recover只有在 defer 调用的函数中才有效

  1. 利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。

  2. recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。

  3. 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。

使用 defer + recover 来捕获和处理异常, 使程序不终止继续运行

package main

import (
	"fmt"
)

func test() {
	// 使用 defer + recover 来捕获和处理异常
	// 定义匿名函数
	defer func() {
		err := recover() // 内置函数 recover() ,可以捕获到异常
		if err != nil {
			fmt.Println("err =>", err)
		}
	}()
	num1 := 10
	num2 := 0
	res := num1 / num2
	fmt.Println("res=", res)

}

func main() {
	// 测试
	test()
	fmt.Println("main() 下面的代码")
}

十 并发编程

1、goroutine(协程)、channels(管道)

进程和线程

  • 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位
  • 线程是进程的一个可执行体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位
  • 一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行

并发和并行

  • 多线程程序在一个核的CPU上运行就是并发
  • 多线程程序在多个核的CPU上运行就行并行

协程和线程

  • 协程:独立的栈空间,共享堆空间,调度由用户自己控制,
  • 线程:一个线程上可以跑多个协程,协程是轻量级的线程

goroutine

goroutine其实就行协程,但是它比线程更小,十几个 goroutine 可能体现在底层就是五六个线程。

当需要某个任务并发执行的时候,只需要把任务封装成一个函数,开启多个 goroutine 去执行这个函数就可以了。

Goroutine 是通过Go 的 runtime 管理的一个线程管理器,goroutine 通过 go 关键字实现,其实就是一个函数(普通函数、匿名函数),只需要在运行的时候前面加上一个 go 关键字,这样就是创建了一个 goroutine

一个 goroutine 必定对应一个函数,但是可以创建多个goroutine去执行相同的函数

package main

import (
	"fmt"
	"time"
)

func Hello() {
	fmt.Println("Hello Goroutine。。。")
}

func main() {

	go Hello() // 启用 goroutine 执行 Hello函数
	
	fmt.Println("Main 入口...")
	time.Sleep(time.Second * 3)
  
	go func() {
		fmt.Println("匿名函数的Goroutine...")
	}()
	time.Sleep(time.Second * 3)

	go Hello() // 再启动一个 goroutine,指向 Hello函数
}

单纯的将函数并发是没有意义的,函数与函数之间需要交换数据才能体现并发执行函数的意义。

如果说 goroutine 是go程序并发的执行体,那么 channel 就是他们之间的连接管道,channel 可以让一个goroutine 发送特定的值到另外一个 goroutine上的通信机制

channel类型

go提供了一种称为通道的机制,用于在goroutine之间共享数据,当执行 goroutine并发活动的时候,需要在groutine之间共享资源或者数据,通道充当goroutine之间的管道并提供一种机制来保证同步交换。

需要在声明通道时指定数据模型,共享内置、命名、结构和引用类型的值和指针,

数据交换的行为,有二种类型的通道

  • 无缓冲通道:无缓冲通道用于执行 goroutine之间的同步
  • 缓存通道:缓存通道用于执行异步通信,

通道由 make函数创建,该函数指定chan关键字和通道元素的类型

创建 channel 语法

// 声明一个传递整型的通道
var ch1 chan int

// 声明一个传递布尔类型的通道
var ch2 chan bool

// 声明一个传递 int 切片类型的通道
var ch3 chan []int

// 声明一个传递 接口类型的通道
var ch4 chan interface{}

// 整型无缓冲通道
stInt := make(chan int)

// 整型有缓存通道
StInt := make(chan int, 10)

// 由内置函数 make 创建,make的第一个参数需要关键字 chan,然后是通道运行交换的数据类型

将值发送到通道 使用 <-

// 字符串缓存通道
str := make(chan string, 10)

// 通过管道发送字符串, 把 Tom 字符串发送到channel str中
str <- "Tom"

// 一个包涵 10 个值的带缓存区的字符串类型的 groutine 通道,然后通过管道发送字符串 “Tom”

从管道接收值

// 从管道接收字符串,从 str 中接收数据,并赋值给 ret
ret := <-str

// <- 运算符附加到通道变量的左侧,以接收来自通道的值

// 关闭通道
close(str)

无缓冲通道

在无缓冲通道中,如果二个groutine没有在同一时刻准备好,则通道会让执行其各自发送和接收操作的groutine等待,同步是通道上发送和接收之间交互的基础,没有另一个则就不能发送

package main

import "fmt"

func NoCache(c chan string) {
	ret := <-c // 从 str 接收值并赋值给变量 ret
	fmt.Println("无缓冲的channel....", ret)
}
func main() {
	// 定义一个无缓冲的 channel str
	str := make(chan string)

	go NoCache(str) // 启用 goroutine 从通道 str 中接收值

	str <- "北京" // 发送值到 str 管道中
	fmt.Println("执行结束")
	close(str)

}

缓存通道

在缓存通道中,有能力接收到一个或者多个值之前保存它们,在这种类型的通道中,无需强制 groutine 在同一时刻准备好执行的接收和发送,

package main

import "fmt"

func main() {
	// 定义容量为 1 有缓存的 channel str,
	str := make(chan string, 1)

	go func() {
		str <- "北京" // 发送值到 str 管道中
	}() // 启用 goroutine 从通道 str 中接收值

	ret := <-str // 把 channel 管道的值 赋值给 ret 变量
	fmt.Println("执行结束,", ret)
	close(str)

}

for range 从channel通道循环取值

package main

import "fmt"

func main() {
	chInt := make(chan int)
	chStr := make(chan int)

	// 开启 goroutine 循环 发送值到  chInt 中
	go func() {
		for i := 0; i < 10; i++ {
			chInt <- i // 取 i值发送给 chInt
		}
		close(chInt) // 关闭管道 chInt
	}()

	// 开启 goroutine 循环从 chInt 中接收值,并将该值发送到 chStr 中
	go func() {
		for {
			i, ok := <-chInt // channel 通道关闭取值 ok = false,停止循环
			if !ok {
				break
			}
			chStr <- i  // 取 i 值 发送给 chStr
		}
		close(chStr) // 关闭管道 chStr
	}()
	// 循环从 chStr 管道中接收值打印
	for i := range chStr {
		fmt.Println("打印:", i)
	}
}

2、WaitGroup

使用 sync.WaitGroup 来实现 goroutine 的同步,

多次执行代码,发现每次打印的数字顺序都不一样,是因为 goroutine 打印是随机的调度的

package main

import (
	"fmt"
	"sync"
)

// 定义个 wg 变量,类型为 sync.WaitGroup
var wg sync.WaitGroup

func Hello(a int) {
	defer wg.Done()           // goroutine 结束就登记 -1
	fmt.Println("Hello: ", a) // 打印 a 的值
}
func main() {
	for i := 0; i < 10; i++ {
		go Hello(i) // 启动一个 goroutine
		wg.Add(1)   // 启动一个 goroutine 就登记 +1
	}

	wg.Wait() // 等待所有登记的 goroutine 都结束

	fmt.Println("main Done")
}

3、runtime

Go运行时的调度器使用GOMAXPROCS参数来确定使用多少个OS线程来同时执行代码,默认是机器上的CPU核心数。

通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数

package main

import (
	"fmt"
	"runtime"
)

func main() {
	// 返回本地机器的逻辑 CPU 个数
	cpuNum := runtime.NumCPU()
	fmt.Println(cpuNum)

	// 设置使用 CPU个数, GOMAXPROCS 设置
	runtime.GOMAXPROCS(cpuNum - 4)
	fmt.Println("ok")
}

设置CPU 核心数,并行执行任务

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var wg sync.WaitGroup

func Test1() {
	defer wg.Done()
	fmt.Println("Test1....")
}

func Test2() {
	defer wg.Done()
	fmt.Println("...Test2")
}

func main() {
	// 设置 CPU个数
	cpuNum := runtime.NumCPU()
	runtime.GOMAXPROCS(cpuNum - 2)

	wg.Add(1) // 启动一个 goroutine 就登记 +1
	go Test1()
	go Test2()
	wg.Wait() // 等待所有的 goroutine 执行结束
	fmt.Println("Main Done")
}

4、select 多路复用

select 类似 switch 语句,有一系列的 case 分支和一个默认的分支,每个 case对应一个通道的通信(接收或者发送)过程,select 会一直等待,直到某个 case 的通信操作完成

  • 使用select 提高代码的可读性
  • 可以同时处理一个或者多个 channel的发送/接收操作
  • 如果多个case 同时满足,select会随机选择一个
package main

import (
	"fmt"
	"time"
)

func main() {
	chInt := make(chan int, 1)
	chStr := make(chan string, 1)

	go func() {
		chInt <- 100
		time.Sleep(time.Second * 5)
		fmt.Println("intType")
	}()
	go func() {
		chStr <- "北京"
		time.Sleep(time.Second * 5)
		fmt.Println("stingType")
	}()

	select {
	case c := <-chInt:
		fmt.Println("Test1 :", c)
	case c := <-chStr:
		fmt.Println("Test2 : ", c)
	default:
		fmt.Println("default.....")
	}

}

5、定时器timer

Ticker 时间到了多次执行

package main

import (
	"fmt"
	"time"
)

func Mon1() {
	fmt.Println("每 10s 执行一次")
}

func Mon2() {
	fmt.Println("每 1min 执行一次")
}
func main() {
	second := time.Tick(time.Second * 10)
	minute := time.Tick(time.Minute * 1)
	for {
		select {
		case <-second:
			Mon1()
		case <-minute:
			Mon2()

		}

	}
}

每 五分钟触发一次


package main

import (
	"fmt"
	"time"
)

func main() {
	minute := time.NewTicker(time.Minute * 5)
	for t := range minute.C {
		fmt.Println("打印", t)
	}

}

十一、文本文件处理

1、目录操作

文件操作的大多数函数都是在os包里面

创建名称为 name 的目录,权限设置是perm,eg:0777

func Mkdir(name string, perm FileMode) error

根据 path 创建多级子目录,eg: /home/vic

func MkdirAll(path string, perm FileMode) error

删除名称为 name 的目录,当目录下有文件或者其他目录的时候会报错

func Remove(name string) error

根据 path 删除多级子目录,如果path 是单个名称,那么该目录下面的子目录全部删除

func RemoveAll(path string) error

示例

package main


import (
	"fmt"
	"os"
)

func main() {
	// 创建目录,等于 mkdir 
	os.Mkdir("victor", 0755)
	
	// 创建目录,等于 mkdir -p
	os.MkdirAll("Victor/vic", 0777)

	// 删除目录
	err := os.Remove("Victor")
	if err != nil {
		fmt.Println(err)
	}
	// 递归删除目录
	os.RemoveAll("Victor")

}

2、创建文件

将数据存储到文件之前,先要创建文件,Go语言中提供了一个 Create() 函数专门创建文件,

该函数在创建文件时,首先会判断要创建的文件是否存储,如果不存在则创建,如果存在会将文件中已有的数据清空。

同时,当文件创建成功后,该文件会默认的打开,所以不用再执行打开操作,可以直接向该文件中写入数据

创建文件的步骤

1、导入 os 包,创建文件,

2、指定创建的文件存放路径以及文件名

3、执行Create()函数,进行文件创建

4、关闭文件

创建文件函数

// 根据提供的文件名创建新的文件,返回一个文件对象,默认权限是 06666 ,返回的文件是可读写的
func Create(name string) (file *File, err Error)

示例:

package main

import (
	"fmt"
	"os"
)

func main() {
	// 创建文件,路径分为绝对路径 和相对路径
	fp, err := os.Create("./test.txt")
	if err != nil {
		fmt.Println("创建文件失败")
		return // 如果 return 出现在主函数中 表示程序的结束
	}

	// 延迟调用,关闭文件句柄
	defer fp.Close()
	fmt.Println("文件创建成功")

}
3、写入数据

写入数据包涵打开文件和写数据

打开文件

// 该方法打开一个名称为 name 的文件,但是是只读的方式,内部实现其实是调用了OpenFile
func Open(name string) (file *File, err Error)

// 打开名称为name 的文件,flag是打开的方式,只读、读写, perm 是权限
func OpenFIle(name string, flag int, perm uint32) (file *File, err Error)

写文件

// 写入 byte 类型的信息到文件
func (file *File) Write(b []byte) (n int, err Error)

// 在指定的位置开始写入 byte 类型的信息
func (file *File) WriteAt(b []byte, off int64) (n int, err Error)

// 写入 string 信息到文件
func (file *File) WriteString(s string) (ret int, err Error)

写文件示例

package main

import (
	"fmt"
	"os"
)

func main() {
	// 创建文件,路径分为绝对路径 和相对路径
	fp, err := os.Create("./test.txt")
	if err != nil {
		fmt.Println("创建文件失败")
		return // 如果 return 出现在主函数中 表示程序的结束
	}

	// 延迟调用,关闭文件句柄
	defer fp.Close()
	fmt.Println("文件创建成功")

	// 将字符串转换成字符切片写入到文件中。字符串和字符切片允许相互转换
	str := "天龙八部\n"

	b := []byte(str)
	fp.Write(b)

	// 字符串的形式直接写入
	fp.WriteString("后羿射日\n")

}

一般都使用OpenFile函数以追加的方式打开一个文件写入

OpenFile 函数有三个参数:文件名、一个或多个标注、使用的文件权限

​ os.O_CREATE 创建,如果指定文件不存在,就创建该文件

​ os.O_RDONLY 只读

​ os.O_WRONLY 只写

​ os.O_TRUNC 截断,如果指定文件已经存在,就将该文件的长度截为 0

package main

import (
	"fmt"
	"os"
)

func main() {
	// 最佳的方式打开文件,如果不存在就创建,打开的模式可读可写,权限 0644
	fp, err := os.OpenFile("./writeFile.txt", os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
	if err != nil {
		fmt.Println("创建文件失败")
		return
	}

	defer fp.Close()

	// 写数据
	fp.WriteString("Go语言基础\n")

	str := "2021年五月"

	b := []byte(str)
	fp.Write(b)

}

4、读取文件

​ 文件是指向os.file 类型的指针来表示,也叫作句柄

​ 标准输入 os.Stdin 和 标准输出 os.Stdout 的类型都是 os.File

在任何计算机设备中,文件必须是对象

读文件函数

// 读取文件到 b 中
func (file *File) Read(b []byte)(n int, err Error)

// 从off开始读取数据到 b 中
func (file *File) ReadAt(b []byte, off int64) (n int, err Error)

读取文件示例

f1,_ : os.ReadFile("./test.txt")
fmt.Println(string(f1[:]))
5、文件操作案例
package main

import (
	"bufio"
	"fmt"
	"io/ioutil"
	"os"
)

func main() {

	f1 := "./writeFile.txt"

	// 将整个文件的内容读取到一个字节切片中
	buf, err := ioutil.ReadFile(f1)
	if err != nil {
		fmt.Fprintf(os.Stderr, "读取文件失败: %s\n", err)
		return
	}

	fmt.Printf("%s\n", string(buf))

	// 读取文件并一行行打印
	f2 := "./test.txt"
	file, err := os.Open(f2)
	if err != nil {
		fmt.Fprintf(os.Stderr, "读取文件失败: %s\n", err)
	}

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}

	// 拷贝源文件内容到新文件
	var (
		oldFile = "./test.txt"
		newFile = "./test2.txt"
	)

	// 将整个文件读取到一个切片中
	tmpFile, err := ioutil.ReadFile(oldFile)
	if err != nil {
		fmt.Fprintf(os.Stderr, "读取文件失败:%s", err)
	}
	// 将读取到的文件内容重新写入到新文件中
	err = ioutil.WriteFile(newFile, tmpFile, 0644)
	if err != nil {
		fmt.Fprintf(os.Stderr, "写入文件失败:%s", err)
	}
}

6、字符串处理操作

https://golang.org/pkg/strings/

对字符 串进行分割、连接、转换等操作

package main

import (
	"fmt"
	"strings"
)

func main() {
	str1 := "hello, golang is dog"

	// 在字符串中查找另外一个字符串是否出现,用于 模糊查找 Contains
	value := strings.Contains(str1, "go")
	if value {
		fmt.Println("找到")
	} else {
		fmt.Println("未找到")
	}

	// 判断是否以某个字符串开头 HasPrefix
	result := strings.HasPrefix(str1, "Hi")
	if !result {
		// 判断是否是 Hi 开头,如果不是就加上
		str1 := fmt.Sprintf("Hi, %s", str1)
		fmt.Println(str1)
	}

	// 判断是否以某个字符串开头 HasSuffix
	ret := strings.HasSuffix(str1, "Night")
	if !ret {
		// 判断是否是 Hi 开头,如果不是就加上
		str1 := fmt.Sprintf("%s ,Night", str1)
		fmt.Println(str1)
	}

	// 字符串拼接 Join
	slice1 := []string{"1313", "3213", "2344", "3242"}

	// 将字符串切片 拼接成字符串
	str2 := strings.Join(slice1, "-")

	fmt.Println(str2)

	// 字符串替换 Replace
	// 在字符串中把 旧字符串 替换成 新字符串,n 表示替换的次数,小于 0 表示全部替换
	fmt.Println(strings.Replace(str1, "golang", "Java", -1))

	// 字符串分割 split
	// 切分字符串返回字符串的切片
	fmt.Println(strings.Split(str1, ","))

	// 字符串全部全换成小写 ToLower
	fmt.Println(strings.ToLower(str1))

	// 字符串全部全换成大写 ToUpper
	fmt.Println(strings.ToUpper(str1))

	// 去掉字符串首尾的空白字符 TrimSpace
	fmt.Println(strings.TrimSpace(str1))

	// 统计字符串出现的次数 Count
	fmt.Println(strings.Count(str1, "golang"))

	// 字符串翻转
	// 先转换成字节切片
	byteArray := []byte(str1)
	s1 := ""
	for i := len(byteArray) - 1; i >= 0; i-- {
		// 转换成字符串
		s1 += string(byteArray[i])
	}
	fmt.Println(s1)
}

7、字符串类型转换

整形之间的转换

/*
		  整形之间的转换
			建议把 低类型 转换成 高类型
	*/
	var a1 int8 = 20
	var b1 int16 = 10
	fmt.Println("sum:", int16(a1)+b1)

浮点型之间的转换

/*
		  浮点型之间的转换
			建议把 低类型 转换成 高类型
	*/
	var a2 float32 = 10
	var b2 float64 = 20
	fmt.Println("浮点型:", float64(a2)+b2)

整形和浮点型之间的转换

/*
		  整形和浮点型之间的转换
			建议把整形转换成浮点型
	*/
	var a3 float32 = 10.25
	var b3 int = 20
	fmt.Println("整形和浮点型之间的转换:", a3+float32(b3))

其他类型换行字符串类型,使用 strconv包进行类型转换

将 bool 类型转换为字符串

// 将 bool 类型转换为字符串

	var str string
	str = strconv.FormatBool(false)
	fmt.Println(str)

将整形转换成字符串

	/*
		 FormatInt 接收二个参数
		 	参数1:int64 类型的值
		 	参数2:int 类型的进制;二进制 、八进制、十进制、十六进制
	*/
	var str string
	
	str = strconv.FormatInt(21123, 10)
	fmt.Println("str = ", str)

	// 第二种方式
	str = strconv.Itoa(21123) // 默认十进制
	fmt.Println("str = ", str)

将浮点数转换成字符串

/*
		 FormatInt 接收四个参数
		 	参数1:要转换的值
		 	参数2:格式化类型 'f'
		 	参数3:保留的小数点(-1 不对小数点格式化)
		 	参数4:格式化的类型,32 或者 64
*/
	s := strconv.FormatFloat(1.3264, 'f', 2, 32)
	fmt.Println(s)

将字符串转换成整形

/*
		string转换成整形
		PaeseInt
		参数1:string数据
		参数2:进制
		参数3:位数 16 32 64
*/
	str := "golang"
	ret, _ := strconv.ParseInt(str, 10, 64)
	fmt.Println(ret)

// 第二种方式
	ret, _ := strconv.Atoi(str) // 默认十进制
	fmt.Println("str = ", ret)

将字符串转换成浮点型

/*
		string转换成浮点型
		ParseFloat
		参数1:string数据
		参数2:位数 32 64
*/
	str5 := "vic"
	num1, _ := strconv.ParseFloat(str5, 64)
	fmt.Printf("值:%v 类型:%T \n", num1, num1)

十二 标准库

1、os 包
package main

import (
    "fmt"
    "os"
)

//https://golang.org/pkg/os/
func main() {
  
    // 终止进程
    // os.Exit(1)
  
  	// 获取全部环境变量
    env := os.Environ()
    
   //获取当前工作目录
    v,_ := os.Getwd()
    fmt.Println(v)

    //将当前工作目录更改为目录("/Users/victor/gitlab")
    //成功切换目录,返回nil,否则报错 chdir 111: no such file or directory
    v1 := os.Chdir("111")
    fmt.Println(v1)

    //更改文件的权限(读写执行,分为三类:all-group-owner)
    v2 :=os.Chmod("/Users/victor/go/121.txt", 755)
    fmt.Println(v2)

    //更改文件拥有者
    v3 :=os.Chown("/Users/victor/go/121.txt", 74, 74)
    fmt.Println(v3)

    //获取主机名
    v4,_ := os.Hostname()
    fmt.Println(v4)

    //创建目录及文件
    os.MkdirAll("/Users/victor/go/test", os.ModePerm)
    os.Chdir("/Users/victor/go/test")
    os.Create("file.txt")
    v5,_ := os.Getwd()
    fmt.Println(v5)

    //删除文件或者目录,如果不存在remove file1.txt: no such file or directory
    // func Remove(name string) error           
    //删除目录以及其子目录和文件,如果path不存在的话,返回nil
    // func RemoveAll(path string) error 
    os.Getwd()
    os.Chdir("/Users/victor/go/test")
    v6 := os.Remove("file1.txt")
    fmt.Println(v6)

    //重命名文件,如果oldpath不存在,则报错no such file or directory
    // func Rename(oldpath, newpath string) error
    os.Chdir("/Users/victor/go/test")
    v7 :=os.Rename("file1.txt", "newfile.txt")
    fmt.Println(v7)

}

利用os.Stat 判断文件是否存在

	ret, err := os.Stat("main.go")
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(ret) // 获取文件的全部信息
	fmt.Println(ret.Name())
	fmt.Println(ret.Size())
	fmt.Println(ret.IsDir())
	fmt.Println(ret.Mode())
}

os.Args 获取命令行参数,是一个切片

package main

import (
    "fmt"
    "os"
)

func main() {
	// (os.Args[0])  // args 0 第一个片 是文件路径
	// (os.Args[1])  // 1 第二个参数是, 用户输入的参数 例如 go run osdemo01.go 123
    fmt.Println("命令行的参数有:", len(os.Args))
    // 遍历 os.Args 切片,就可以得到所有的命令行输入的参数值
    for k, v := range os.Args {
        fmt.Printf("Args[%v] = %v \n", k, v)
    }
}

// 执行结果
╰─$ go build -o args_demo main.go

╰─$ ./args_demo 31 24 vic
命令行的参数有: 4
Args[0] = ./args_demo
Args[1] = 31
Args[2] = 24
Args[3] = vic
2、ioutil包

封装了一些实用的I/O函数

https://pkg.go.dev/io/ioutil#pkg-functions

名称作用
ReadAll读取数据,返回读取到的字节切片(slice)
ReadDir读取目录,返回目录入口数的数组[]os.FileInfo
ReadFile读取文件,返回文件内容字节(slice)
WriteFile根据文件路径写入字节(slice)
TempDir在一个目录中创建指定前缀名的临时目录,返回新临时目录的路径
TempFile在一个目录中创建指定前缀的临时文件,返回os.File

ReadAll读取接收到的数据

package main

import (
	"fmt"
	"io/ioutil"
	"strings"
)

func main() {
	r := strings.NewReader("Golang is a very good language")
	b, _ := ioutil.ReadAll(r)
	fmt.Printf("%s \n", b)
	fmt.Println(string(b))
}

示例

package main

import (
	"fmt"
	"io/ioutil"
)

func main() {
	// ReadDir 读取目录,返回目录下的数组切片
	fs, _ := ioutil.ReadDir(".")
	// 循环打印文件名
	for _, i := range fs {
		fmt.Println(i.Name())
	}

	// ReadFile 读取文件内容
	content, _ := ioutil.ReadFile("main.go")
	fmt.Println(string(content))

	// WriteFile 将数据写入到传递的文件命文件中,如果不存在就创建
	// 用byte接收数据
	msg := []byte("Hello, Golang language!")
	err := ioutil.WriteFile("hello.txt", msg, 0644)
	if err != nil {
		fmt.Println(err)
	}

	// TempFile 创建临时文件, 文件名是通过 pattern 并在末尾添加一个随机字符串来生成的
	tmpFile, _ := ioutil.TempFile(".", "ccg")
	// defer os.RemoveAll(tmpFile.Name()) // 最后清理删除临时文件

	// 写入 msg 数据
	if _, err := tmpFile.Write(msg); err != nil {
		fmt.Println(err)
	}

}

3、bufio包

bufio 包实现了又缓冲的I/O,它包装了io.Readerio.Writer接口对象

默认缓存defaultBufSize = 4096

  • Reader实现了给一个io.Reader接口对象附加缓冲

  • NewReaderSize创建一个具有默认大小缓冲、从r读取的*Reader。NewReader相当于NewReaderSize(rd, 4096)

读操作

package main

import (
	"bufio"
	"fmt"
	"strings"
)

func main() {
	// 创建一个 newReader 数据,可以读取文件
	str := "hello golang language word\n"
	//f, _ := os.Open("hello.txt")
	str1 := strings.NewReader(str)

	// 使用 bufio.NewReader 进行封装已经存在的数据
	str2 := bufio.NewReader(str1)

	// 以字符串的方式读取,以 '\n' 结尾,或者读到文件尾
	s, _ := str2.ReadString('\n')
	fmt.Println(s)

	// Reset丢弃缓冲中的数据, 重写内容
	str3 := "Golang language"
	str1.Reset(str3) // 将 str3 重写入到 缓存中
	s, _ = str2.ReadString('\n')
	fmt.Println(s)

	// ReadSlice读取直到第一次遇到delim字节,返回缓冲里的包含已读取的数据和delim字节的切片
	s1 := strings.NewReader("hello Go java")
	s2 := bufio.NewReader(s1)
	s3, _ := s2.ReadSlice(' ')
	fmt.Println(string(s3))
	s3, _ = s2.ReadSlice(' ')
	fmt.Println(string(s3))
}

写操作

package main

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
)

func main() {
	f, _ := os.OpenFile("hello.txt", os.O_RDWR, 0644) // openfile 封装了 Reader Writer
	fw := bufio.NewWriter(f)                          // 创建一个bufio 写的缓存取,封装 f
	fw.WriteString("\n Go go go word")                // 写入数据
	fw.Flush()                                        // 刷新缓冲区

	// 利用 bytes 进行封装
	b := bytes.NewBuffer(make([]byte, 0))
	bw := bufio.NewWriter(b)
	bw.WriteString("Hello go")
	bw.Flush() // 刷新缓冲区
	fmt.Println(b)
}

Scanner类型提供了方便的读取数据的接口

将文件分割为行、字节、unicode码值、空白分隔的word

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	//s := strings.NewReader("ABC DE FG") // 定义一个Reader,也可以是文件
	f, _ := os.OpenFile("hello.txt", os.O_RDWR, 0644) // openfile 封装了 Reader Writer
	fs := bufio.NewScanner(f)                         // 创建一个bufio 缓存区,封装 f
	fs.Split(bufio.ScanWords)                         // 以 单词进行分割,或者bufio.ScanRunes、
	for fs.Scan() {
		fmt.Println(fs.Text())
	}
}

4、bytes包

bytes包提供了对字节切片进行读写操作的一系列函数,基本处理函数、比较函数、后缀检查函数、索引函数、分割函数、大小写处理函数、子切片处理函数,和 strings 类似

  • bytes.Contains 是否包涵
  • bytes.count 统计
  • bytes.Join 切片拼接
package main

import (
	"bytes"
	"fmt"
)

func main() {

	var a int = 100
	var b byte
	b = byte(a) // 强制类型转换
	fmt.Println(b)
	fmt.Printf("%T \n", b)

	s := "hello world"
	sb := []byte(s) // 将字符串转换成字节切片
	sb1 := []byte("ll")
	// sb1 是否在 sb 里面
	t := bytes.Contains(sb, sb1)
	fmt.Println(t)

	// bytes.Runes 可以统计有多少 汉字 或者字节
	sh := "欢迎北京"
	sy := []byte(sh)
	sy1 := bytes.Runes(sy)
	fmt.Println(len(sh))
	fmt.Println(len(sy1))

	// join 拼接 切片
	sj1 := [][]byte{[]byte("你好"), []byte("上海")}
	sj2 := []byte(",")
	sj := bytes.Join(sj1, sj2)
	fmt.Println(string(sj))
}

Reader

package main

import (
	"bytes"
	"fmt"
)

func main() {

	data := "hello world"
	//通过[]byte创建Reader
	ret := bytes.NewReader([]byte(data))
	// 初始化一个切片,4个容量
	buf := make([]byte, 4)
	for {
		// 读取数据
		n, err := ret.Read(buf)
		// 读取不到数据,停止
		if err != nil {
			break
		}
		fmt.Println(string(buf[:n]))
	}

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
学习 Golang 后端开发需要掌握以下几个方面: 1. Golang 语言基础:学习 Golang 的语法、数据类型、控制流、函数、接口等基础知识。 2. Web 开发框架:了解 Golang 常用的 Web 开发框架,如 Gin、Echo、Beego 等。 3. 数据库操作:学习 Golang 如何操作 MySQL、PostgreSQL、MongoDB 等数据库。 4. 缓存技术:了解 Redis 等常用缓存技术的使用和优化。 5. 消息队列:学习消息队列的使用,如 RabbitMQ、Kafka 等。 6. 微服务架构:了解微服务架构的设计和实现方式,如 gRPC、Consul、Zookeeper 等。 7. 安全性:了解如何保证 Golang 后端应用的安全性,包括数据传输的加密、防止 SQL 注入、XSS 攻击等。 具体的学习路线可以按照以下步骤进行: 1. 先学习 Golang 基础知识,可以参考《Go 语言圣经》或《Go 语言编程》等经典教材。 2. 掌握 Web 开发框架,可以从 Gin 或 Echo 开始,掌握基本的 API 开发方式。 3. 学习数据库操作,可以从 MySQL 开始,了解如何使用 Golang 连接数据库、执行 SQL 语句等。 4. 学习缓存技术和消息队列,可以从 Redis 和 RabbitMQ 开始,了解如何使用这些技术提高系统性能和可靠性。 5. 学习微服务架构,可以了解如何使用 gRPC、Consul、Zookeeper 等工具实现微服务架构。 6. 学习安全性,可以了解如何使用 TLS 加密数据传输、如何防止 SQL 注入、XSS 攻击等常见安全问题。 以上是一个简单的学习路线,具体的学习内容和顺序可以根据自己的实际情况进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值