go语言基础笔记

一、前提

1、go命令集

go run

go run hello.go

执行go run命令的时候会先将hello.go编译成二进制文件,该二进制文件被创建在一个临时目录中,从临时命令中执行二进制文件,然后程序结束后删除二进制文件

go build

go build hello.go

go build会在当前目录下创建一个名为hello的可执行文件,也可指定-o指定可执行文件的名字。

go get golang.org/x/lint/golint@latest

go get/install

go get golang.org/x/lint/golint@latest
go install golang.org/x/lint/golint@latest

go get/install 用来安装三方应用

代码格式化

go fmt

go fmt可以重新格式化代码,可以修正缩紧的空格,排列结果中的字段。

// -l是将格式不正确的文件打印到控制台,-w在原路径直接修改文件, .表示要扫描的文件为当前目录及所有子目录的所有文件
goimports -l -m .

goimports是增强版的go fmt,它可以将导入语句按字母排列,删除导入语句,也可以尝试猜测任何未指定的导入。

代码分析和审查

//代码审查的一部分
golint ./..
//自动构建的必要部分
go vet ./..

golint试图确保代码遵循风格标准,但是golint发现的问题比较模糊,所以作为建议,不必完全参照修改;
go vet 可以发现代码中的一些错误,尽管代码格式正确,但是出现了一些不愿看到的错误,比如给格式化方法传递了错误的参数数量,或者给从未使用过的变量赋值。

2、分号插入规则

go其实与java一样,也是需要在每条语句后面加上分号,但是我们实际在写go代码的时候是不用自行添加分号,go编译器会在编译的时候按照Effective Go中描述的规则添加分号。

如果换行之前的最后一个标记是以下任何一种的时候会插入分号:

  • 标识符,包括int和float64等词;
  • 基本字面量,如数字或字符床;
  • 以下标识之一:break、continue、fallthrough、return、++、–、)或}。
    所以就知道为啥把括号放在不正确的位置会导致错误,写代码的时候自己注意。

二、基础类型和变量

1、内置类型

字面量

go语言中一个值的字面形式成为一个字面量,一个值kennel有很多种字面量形式,它可能是数字(整数和浮点)、字符或字符串等。

整数字面量

  • 整数在默认情况下是十进制的数字序列,但是使用不同的特定前缀来表示不同进制的数,比如0b/0B表示二进制,0o/0O表示八进制,0x/0X表示十六进制;
  • 较长的整数可以用下划线_做间隔,一般在千分位,比如1234可以写成1_234;

浮点数字面量

  • 由整数部分、小数点、小数部分组成,也可以表示成指数形式,比如6.03e23;

字符字面量

  • 由单引号包围的字符组成;(注意:go语言中单引号和双引号表示的是不同的字面量);

字符串字面量

  • 有两种格式:原始字符串字面量(反引号风格)和解释型字符串字面量(双引号风格)
  • 解释型字符串字面量:"这是一个解释型字符串字面量的字符串"
  • 原始字符串字面量:`这是一个反引号风格的字符串`;(反引号有个优势就是可以包含任何除反引号之外的任意字符)

布尔型
bool变量只有2种状态:true和false。

数据类型

整数:整数分为带符号整型和无符号整型

  • uint8:无符号 8 位整型 (0 到 255)
  • uint16:无符号 16 位整型 (0 到 65535)
  • uint32:无符号 32 位整型 (0 到 4294967295)
  • uint64:无符号 64 位整型 (0 到 18446744073709551615)
  • int8:有符号 8 位整型 (-128 到 127)
  • int16:有符号 16 位整型 (-32768 到 32767)
  • int32:有符号 32 位整型 (-2147483648 到 2147483647)
  • int64:有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)
  • byte:类似 uint8,一般使用byte更加直观
  • rune:类似 int32
  • uint:32 或 64 位
  • int:与 uint 一样大小
  • uintptr:无符号整型,用于存放一个指针

使用整型的三条规则:

  • 在处理二进制文件或者实现网络协议时,请根据实际需要的变量大小和有无符号选择对应的整型;
  • 如果正在编写一个可适用于任何整型的库函数,通常推荐写一对函数,一个函数的参数和变量位int64,另一个为uint64;
  • 其他情况下,int可以通用;

整数的操作符为+-*/%(可以简化为+= -= *= /= %=),也可使用位运算符:<<(左移) >>(右移) &(与) |(或) ^ (异或) &^(与非) ,他们也可以与等号结合使用。

浮点型:支持两种浮点型float32和float64:

  • float32:IEEE-754 32位浮点型数
  • float64:IEEE-754 64位浮点型数(精度高,尽量使用这个)

浮点型可以进行除%以外的任意数字运算和比较运算,当非0的数除以0时,将返回+Inf(正无穷大)或-Inf(负无穷大),0/0=NaN。

复数:一般很少使用

  • complex64:32 位实数和虚数
  • complex128:64位实数和虚数

字符串:go支持unicode编码。
一个字符串是一个不可改变的字节序列。字符串可以包含任意的数据,包括byte值0,但是通常是用来包含人类可读的文本

因为Go语言源文件总是用UTF8编码,并且Go语言的文本字符串也以UTF8编码的方式处理,因此我们可以将Unicode码点也写到字符串面值中。

1、内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目)

s:="hello, world"
len(s)
-----输出----
12
a:="hello"
a[0] 的值是字节值,104 而不是'h'

重点:strings包、strconv 字符转换
了解:bytes包和unixode包
unicode/utf-8:抽空研究下

printf
%d 十进制整数
%x, %o, %b 十六进制,八进制,二进制整数。
%f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00
%t 布尔:true或false
%c 字符(rune) (Unicode码点)
%s 字符串
%q 带双引号的字符串"abc"或带单引号的字符’c’
%v 变量的自然形式(natural format)
%T 变量的类型
%% 字面上的百分号标志(无操作数)

2、变量

标识符用来命名变量、类型等程序实体,一个标识符实际上就是一个或是多个字母(AZ和az)数字(0~9)、下划线_组成的序列,但是第一个字符必须是字母或下划线而不能是数字。

//有效的标识符
test 
_test
test123
//无效的标识符
123test
case(Go 语言的关键字)

在函数内部,建议使用简短的变量名,变量的范围越小,使用的名称越短,所以在go中单字母变量名十分常见;

1)声明

使用var关键字声明变量:

//申明单个变量
var a string = "Runoob"
//一次申明多个变量
var b, c int = 1, 2

//如果等号右侧是预期的数据类型,则可以省略类型
var a = "Runoob"

//只声明变量,没赋值时 变量是零值;
var a string   //等同:a = ""

使用:=(短声明格式)声明变量:

//以下两行代码都是声明一个值为10的整型变量x
var x = 10
x := 10

//声明多个变量
x,y := 12,"hello"

//允许对已经赋值的变量再次赋值,之前要等号左边有任意一个新变量即可
x:=10
x,y := 30,"hello"

注意:

  • 短声明格式(:=)只能用来声明在函数内的局部变量,在包级别声明变量只能只用var关键字;

  • 以下情况请避免使用:=

    • 当需要初始化一个变量为零值时,使用var x int,这可以清晰的表达了零值的意图;
    • 当需要将无类型常量或者字面量赋值给一个变量时,如果常量或者字面量的默认类型不是变量的预期类型,需要使用var标准形式声明变量,尽管可以使用:=,但是建议使用var;
    • 因为:=既可以赋值给新变量也可以赋值给现有的变量,所以会导致当你认为在重用现在变量时,却在创建变量;

2)常量

使用关键字const声明常量

//声明单个常量
const x int64 =10

// 声明多个常量,例如:常量做枚举, 0、1 和 2 分别代表未知性别、女性和男性
const (
    Unknown = 0
    Female = 1
    Male = 2
)

赋值规则:

  • 数字字面量
  • true和false
  • 字符串
  • 字符
  • 内置函数complex、real、img、len和cap;
  • 包含预声明(内置)的值和运算符的表达式;

常量分为无类型常量和有类型常量

//无类型常量,由于x的类型不确定,所以下面的赋值都是合法的
const x = 10
var y int = x
var z float64 = x
var d byte = x

//有类型常量 typedX是一个有类型的常量(int类型),并且只能使用int类型的值对其赋值
const typedX int = 10

3)枚举

在go语言中没有枚举类型,但是我们可以使用const + iota(常量累加器)来进行模拟。

1.iota是常量组计数器;
2.iota从0开始,每换行递增1;
3.常量组有个特点如果不赋值,默认与上一行表达式相同
4.如果同一行出现两个iota,那么两个iota的值是相同的
5.每个常量组的iota是独立的,如果遇到const iota会重新清零

//模拟一个一周的枚举
const (
	MONDAY    = iota       //0
	TUESDAY   = iota       //1
	WEDNESDAY = iota       //2
	THURSDAY               //3  ==> 没有赋值,默认与上一行相同iota ==> 3
	FRIDAY                 //4
	SATURDAY               //5
	SUNDAY                 //6
	M, N      = iota, iota //const属于预编译期赋值,所以不需要:=进行自动推导
)

const (
	JANU = iota + 1 //1
	FER             //2
	MAR             //3
	APRI            //4
)

func main() {

	fmt.Println("打印周:")
	fmt.Println(MONDAY)
	fmt.Println(TUESDAY)
	fmt.Println(WEDNESDAY)
	fmt.Println(THURSDAY)
	fmt.Println(FRIDAY)
	fmt.Println("M:", M, ",N:", N)

	fmt.Println("打印月份:")
	fmt.Println(JANU) //1
	fmt.Println(FER)  //2
	fmt.Println(MAR)  //3
}

3、指针

go语言在使用指针时会是使用内部的垃圾回收机制(gc:garbage collector),开发人员不需要手动释放内存,go可以返回栈上的指针,程序在编译的时候就确定了变量的分配位置。

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址

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

var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮点型 */
name := "xiaoli"
//指针变量的内存地址
ptr := &name
fmt.Println(ptr) //0xc000010200
fmt.Println(*ptr) //xiaoli

使用new关键字定义

//先声明指针
name2Ptr := new(string)
//赋值
*name2Ptr = "xiaowang"
fmt.Println(name2Ptr)
fmt.Println(*name2Ptr)

//可以返回栈上的指针,编译器在编译程序时,会自动判断这段代码,将city变量分配在堆上, 内存逃逸
res := testPtr()
fmt.Println("res city :", *res, ", address:", res)

空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil,指代零值或空值

4、数组

数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。

声明数组

var variable_name [SIZE] variable_type

//声明数组
var score [10] int

//定义一个具有10个数字的数组
nums := [10]int{1, 2, 3, 4}

访问数组元素

//直接通过索引访问 nums[1]
var salary int = nums[1]

//遍历方式一:
for i:=0;i<len(nums);i++{
	fmt.Println(i,nums[i])
}
//遍历方式二:k是数组下标,v是数组的值
for k,v :=range nums{
	fmt.Println(k,v)
}

5、切片

切片:slice,它的底层也是数组,可以动态改变长度。

//使用var定义切片,切片不需要说明长度。
var identifier []type

//使用make定义切片
var slice1 []type = make([]type, len)
//上面的简写
slice1 := make([]type, len)
//定义切片
names := []string{"北京", "上海", "广州", "深圳"}

//追加数据
names1 := append(names,"辽宁")
fmt.Println(names1)

//对于一个切片,不仅有'长度'的概念len(),还有一个'容量'的概念cap()
names := []string{"北京", "上海", "广州", "深圳"}
fmt.Println("追加元素前,name的长度:",len(names),cap(names))
names = append(names,"辽宁")
fmt.Println("追加元素后,name的长度:",len(names),cap(names))

//切片可以基于一个数组,灵活的创建新的数组,此时的names2是软拷贝,修改names2中的值后,names也随之改变
names2 := names[0:3] //左闭右开
names2[1] = "杭州"
fmt.Println("修改names[2]之后, names2:", names2)
fmt.Println("修改names[2]之后, names:", names)

//如果想让切片完全独立于原始数组,可以使用copy()函数来完成
namesCopy := make([]string, len(names))
copy(namesCopy, names[:])
fmt.Println("namesCopy:", namesCopy)

6、Map

Map 是一种无序的键值对的集合(相当于python的字典)。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

//1、定义一个map,此时这个map是不能直接赋值的,它是空的
var idNames map[int]string 

//2、分配空间,使用make,可以不指定长度,但是建议直接指定长度,性能更好
idScore := make(map[int]float64)   //这个也是正确的
idNames = make(map[int]string, 10) //建议使用这种方式

//3、定义时直接分配空间
//idNames1 := make(map[int]string, 10) //这是最常用的方法

//追加数据
idNames[0]="xiaoli"
idNames[1] = "lily"

//4、遍历map
for k,v :=range idNames{
	fmt.Println(k,v)
}

//5、如何确定一个key是否存在map中
//在map中不存在访问越界的问题,它认为所有的key都是有效的,所以访问一个不存在的key不会崩溃,返回这个类型的零值
//零值:  bool=》false, 数字=》0,字符串=》空
name9 := idNames[9]
fmt.Println("name9:", name9)               //空
fmt.Println("idScore[100]:", idScore[100]) //0

//无法通过获取value来判断一个key是否存在,因此我们需要一个能够校验key是否存在的方式
value, ok := idNames[1] //如果id=1是存在的,那么value就是key=1对应值,ok返回true, 反之返回零值,false
if ok {
	fmt.Println("id=1这个key是存在的,value为:", value)
}

value, ok = idNames[10]
if ok {
	fmt.Println("id=10这个key是存在的,value为:", value)
} else {
	fmt.Println("id=10这个key不存在,value为:", value)
}

//6、删除map中的元素
//使用自由函数delete来删除指定的key
fmt.Println("idNames删除前:", idNames)
delete(idNames, 1)   //"lily"被kill
delete(idNames, 100) //不会报错
fmt.Println("idNames删除后:", idNames)

//并发任务处理的时候,需要对map进行上锁 //TODO

7、函数

1)函数

函数是基本的代码块,用于执行一个任务,go语言最少有个 main() 函数。

你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务。

//Go 语言函数定义格式如下:
func function_name( [parameter list] ) [return_types] {
   函数体
}
  • func:函数由 func 开始声明
  • function_name:函数名称,参数列表和返回值类型构成了函数签名。
  • parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
  • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
    函数体:函数定义的代码集合。

//1.函数返回值在参数列表之后
//2.如果有多个返回值,需要使用圆括号包裹,多个参数之间使用,分割
func test2(a int, b int, c string) (int, string, bool) {
	return a + b, c, true
}

func test3(a, b int, c string) (res int, str string, bl bool) {

	var i, j, k int
	i = 1
	j = 2
	k = 3
	fmt.Println(i, j, k)

	//直接使用返回值的变量名字参与运算
	res = a + b
	str = c
	bl = true

	//当返回值有名字的时候,可以直接简写return
	return

	//return a + b, c, true
}

//如果返回值只有一个参数,并且没有名字,那么不需要加圆括号
func test4() int {
	return 10
}

func main() {
	v1, s1, _ := test2(10, 20, "hello")
	fmt.Println("v1:", v1, ",s1:", s1)

	v3, s3, _ := test3(20, 30, "world")
	fmt.Println("v3:", v3, ", s3:", s3)
}

2)init函数

  • init函数没有参数,没有返回值,原型固定如下
  • 一个包中包含多个init时,调用顺序是不确定的(同一个包的多个文件中都可以有init)
  • init函数时不允许用户显示调用的
  • 有的时候引用一个包,可能只想使用这个包里面的init函数(mysql的init对驱动进行初始化),但是不想使用这个包里面的其他函数,为了防止编译器报错,可以使用_形式来处理import _ "xxx/xx/sub"
package sub
import "fmt"

func init() {
	fmt.Println("this is first init() in package sub ==> sub.go")
}

func init() {
	fmt.Println("this is second init() in package sub ==> sub.go ")
}

//在go语言中,同一层级目录,不允许出现多个包名
func Sub(a, b int) int {
	//init() ==> 不允许显示调用
	return a - b
}
package main

import (
	_ "day02/05-init函数/sub" //此时,只会调用sub里面的init函数,编译还不会出错
	"fmt"
)

func main() {
	//res := sub.Sub(10, 5)
	//fmt.Println("sub.Sub(20,10) =", res)
	fmt.Println("Hello world")
}

8、os.Args

直接可以获取命令输入,是一个字符串切片 []string

cmds := os.Args
//os.Args[0] ==> 程序名字
//os.Args[1] ==> 第一个参数 ,以此类推
for key, cmd := range cmds {
fmt.Println("key:", key, ", cmd:", cmd, ", cmds len:", len(cmds))
}

9、标签LABEL

Go语言支持label(标签)语法:分别是break label和 goto label 、continue label

func main() {
	//标签 LABEL1
	//goto LABEL  ===> 下次进入循环时,i不会保存之前的状态,重新从0开始计算,重新来过
	//continue LABEL1 ===> continue会跳到指定的位置,但是会记录之前的状态,i变成1
	//break LABEL1  ==> 直接跳出指定位置的循环

//标签的名字是自定义的,后面加上冒号
LABEL121:
	for i := 0; i < 5; i++ {
		for j := 0; j < 5; j++ {
			if j == 3 {
				//goto LABEL1
				//continue LABEL1
				break LABEL121
			}

			fmt.Println("i:", i, ",j:", j)
		}
	}

	fmt.Println("over!")
}

10、结构体

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

//go语言结构体使用type + struct来处理
type Student struct {
	name   string
	age    int
	gender string
	score  float64
}

func main() {
	var i, j MyInt
	i, j = 10, 20

	fmt.Println("i+j:", i+j)

	//创建变量,并赋值
	lily := Student{
		name:   "Lily",
		age:    20,
		gender: "女生",
		//score:  80, //最后一个元素后面必须加上逗号,如果不加逗号则必须与}同一行
		//}
		score: 80} //最后一个元素后面必须加上逗号,如果不加逗号则必须与}同一行

	//使用结构体各个字段
	fmt.Println("lily:", lily.name, lily.age, lily.gender, lily.score)

	//结构体没有-> 操作
	s1 := &lily
	fmt.Println("lily 使用指针s1.name打印:", s1.name, s1.age, s1.gender, s1.score)
	fmt.Println("lily 使用指针(*s1).name打印:", (*s1).name, s1.age, s1.gender, s1.score)

	//在定义期间对结构体赋值时,如果每个字段都赋值了,那么字段的名字可以省略不写
	//如果只对局部变量赋值,那么必须明确指定变量名字
	Duke := Student{
		name: "Duke",
		age:  28,
		//"男生",
		// 99,
	}
	Duke.gender = "男生"
	Duke.score = 100

	fmt.Println("Duke:", Duke)
}

11、延迟defer

package main

import (
	"fmt"
	"os"
)

func main() {
	//1.延迟,关键字,可以用于修饰语句,函数,确保这条语句可以在当前栈退出的时候执行
	//2. 一般用于做资源清理工作
	//3. 解锁、关闭文件
	//4. 在同一个函数中多次调用defer,执行时类似于栈的机制:先后入后出

	filename := "01-switch.go"
	readFile(filename)
}

func readFile(filename string) {
	//func Open(name string) (*File, error) {
	//1. go语言一般会将错误码作为最后一个参数返回
	//2. err一般nil代表没有错误,执行成功,非nil表示执行失败
	f1, err := os.Open(filename)

	//匿名函数,没有名字,属于一次性的逻辑 ==> lamada表达式
	defer func(a int) {
		fmt.Println("准备关闭文件, code:", a)
		_ = f1.Close()
	}(100) //创建一个匿名函数,同时调用

	if err != nil {
		fmt.Println("os.Open(\"01-switch.go\") ==> 打开文件失败, err:", err)
		return
	}

	defer fmt.Println("0000000")
	defer fmt.Println("1111111")
	defer fmt.Println("2222222")

	buf := make([]byte, 1024)
	//func (f *File) Read(b []byte) (n int, err error) {
	n, _ := f1.Read(buf)
	fmt.Println("读取文件的实际长度:", n)
	fmt.Println("读取的文件内容:", string(buf))

}

三、条件语句

1、if语句

if 布尔表达式 {
   /* 在布尔表达式为 true 时执行 */
} else {
  /* 在布尔表达式为 false 时执行 */
}
a := 10
if a >= 10 {
	fmt.Println("11111")
}else {
	fmt.Println("22222")
}

2、switch语句

  • witch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止;

  • switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面也不需要再加 break;

  • 匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough

arrs := []int{12,7,9,10}
switch arrs[1] {
case 7:
	fmt.Println("111111")
	//默认加上break了,不需要手动处理
	//如果想向下穿透的话,那么需要加上关键字: fallthrough
	fallthrough
case 12:
	fmt.Println("222222")
case 15:
	fmt.Println("333333")
default:
	fmt.Println("default called!")
}

3、select语句

类似switch语句,select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收;

select {
  case <- channel1:
    // 执行的代码
  case value := <- channel2:
    // 执行的代码
  case channel3 <- value:
    // 执行的代码

    // 你可以定义任意数量的 case

  default:
    // 所有通道都没有准备好,执行的代码
}
  • 每个case都是一个通道;
  • 所有channel表达式都会被求指;
  • 所有被发送的表达式都会被求值;
  • 如果任意某个通道可以进行,他就执行,其他被忽略;
  • 如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行,否则:
    • 如果有default子句,则执行该语句;
    • 如果没有default子句,select将阻塞,直到某个通道可以运行;go不会重新对channel或值进行求值;

四、类

1、封装

package main

import "fmt"


type Person struct {
	//成员属性:
	//在go语言中,权限都是通过首字母大小来控制,小写的name在外部是访问不到的
	name   string
	age    int
	gender string
	score  float64
}

//在类外面绑定方法
func (this *Person) Eat() {
	//fmt.Println("Person is eating")
	//类的方法,可以使用自己的成员
	//fmt.Println(this.name + " is eating!")
	this.name = "Duke"
}

func (this Person) Eat2() {
	fmt.Println("Person is eating")
	//类的方法,可以使用自己的成员
	this.name = "Duke"
}

func main() {
	lily := Person{
		name:   "Lily",
		age:    30,
		gender: "女生",
		score:  10,
	}

	lily1 := lily

	fmt.Println("Eat,使用p *Person,修改name的值 ...")
	fmt.Println("修改前lily:", lily) //lily
	lily.Eat()
	fmt.Println("修改后lily:", lily) //Duke

	fmt.Println("Eat2,使用p Person,但是不是指针 ...")
	fmt.Println("修改前lily:", lily1) //lily
	lily1.Eat2()
	fmt.Println("修改后lily:", lily1) //lily

	var myint1 MyInt1 = 100
	myint1.printMyInt()
}

2、继承

import "fmt"

//在go语言中,权限都是通过首字母大小来控制
//1. import ==》 如果包名不同,那么只有大写字母开头的才是public的
//2. 对于类里面的成员、方法===》只有大写开头的才能在其他包中使用

type Human struct {
	//成员属性:
	Name   string
	Age    int
	Gender string
}

//在类外面绑定方法
func (this *Human) Eat() {
	fmt.Println("this is :", this.Name)
}

//定义一个学生类,去嵌套一个Hum
type Student1 struct {
	Hum    Human //包含Human类型的变量, 此时是类的嵌套
	Score  float64
	School string
}

//定义一个老师,去继承Human
type Teacher struct {
	Human          //直接写Huam类型,没有字段名字
	Subject string //学科
}

3、多态

接口


import "fmt"

//在C++中,实现接口的时候,使用纯虚函数代替接口
//在go语言中,有专门的关键字 interface来代表接口
//interface不仅仅是用于处理多态的,它可以接受任意的数据类型,有点类似void

func main() {
	//func Println(a ...interface{}) (n int, err error) {
	fmt.Println("")

	//var i,j,k int
	//定义三个接口类型
	var i, j, k interface{} //空接口
	names := []string{"duke", "lily"}
	i = names
	fmt.Println("i代表切片数组:", i)

	age := 20
	j = age
	fmt.Println("j代表数字:", j)

	str := "hello"
	k = str
	fmt.Println("k代表字符串:", k)

	//我们现在只知道k是interface,但是不能够明确知道它代表的数据的类型
	kvalue, ok := k.(int)
	if !ok {
		fmt.Println("k不是int")
	} else {
		fmt.Println("k是int, 值为:", kvalue)
	}

	//最常用的场景: 把interface当成一个函数的参数,(类似于print),使用switch来判断用户输入的不同类型
	//根据不同类型,做相应逻辑处理

	//创建一个具有三个接口类型的切片
	array := make([]interface{}, 3)
	array[0] = 1
	array[1] = "Hello world"
	array[2] = 3.14

	for _, value := range array {
		//可以获取当前接口的真正数据类型
		switch v := value.(type) {
		case int:
			fmt.Printf("当前类型为int, 内容为:%d\n", v)
		case string:
			fmt.Printf("当前类型为string, 内容为: %s\n", v)
		case bool:
			fmt.Printf("当前类型为bool, 内容为: %v\n", v) //%v可以自动推到输出类型
		default:
			fmt.Println("不是合理的数据类型")
		}
	}
}

多态实现:
go语言的多态不需要继承,只要实现相同的接口即可

import "fmt"

//实现go多态,需要实现定义接口
//人类的武器发起攻击,不同等级子弹效果不同

//定义一个接口, 注意类型是interface
type IAttack interface {
	//接口函数可以有多个,但是只能有函数原型,不可以有实现
	Attack()
	//Attack1()
}

//低等级
type HumanLowLevel struct {
	name  string
	level int
}

func (a *HumanLowLevel) Attack() {
	fmt.Println("我是:", a.name, ",等级为:", a.level, ", 造成1000点伤害")
}

//高等级
type HumanHighLevel struct {
	name  string
	level int
}

func (a *HumanHighLevel) Attack() {
	fmt.Println("我是:", a.name, ",等级为:", a.level, ", 造成5000点伤害")
}

//定义一个多态的通用接口,传入不同的对象,调用同样的方法,实现不同的效果 ==》 多态
func DoAttack(a IAttack) {
	a.Attack()
}

func main() {
	//var player interface{}
	var player IAttack //定义一个包含Attack的接口变量

	lowLevel := HumanLowLevel{
		name:  "David",
		level: 1,
	}

	highLevel := HumanHighLevel{
		name:  "David",
		level: 10,
	}

	lowLevel.Attack()
	highLevel.Attack()

	//对player赋值为lowLevel,接口需要使用指针类型来赋值
	player = &lowLevel
	player.Attack()

	player = &highLevel
	player.Attack()

	fmt.Println("多态......")
	DoAttack(&lowLevel)
	DoAttack(&highLevel)
}
  1. 定义一个接口,里面设计好需要的接口,可以有多个(Attack (), Attack()1 …)

  2. 任何实现了这个接口的类,都可以赋值给这个接口,从而实现多态

  3. 多个类之间不需要有继承关系

  4. 如果interface中定义了多个接口,那么实际的类必须全部实现接口函数,才可以赋值

五、并发

并发:电脑同时听歌,看小说,看电影。cpu根据时间片进行划分,交替执行这个三个程序。我们人可以感觉是同时产生的。
并行:多个CPU(多核)同时执行

c语言里面实现并发过程使用的是多线程(C++的最小资源单元)和进程,而go语言里面不是线程,而是go程 ==> goroutine,go程是go语言原生支持的。
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
每一个go程占用的系统资源远远小于线程,一个go程大约需要4K~5K的内存资源

1、goroutine

goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

go 函数名( 参数列表 )
//示例:
go func(x, y, z)
package main

import (
	"fmt"
	"time"
)

//这个将用于子go程使用
func display(num int) {
	count := 1
	for {
		fmt.Println("=============> 这是子go程:", num, "当前count值:", count)
		count++
		//time.Sleep(1 * time.Second)
	}
}

func main() {
	//启动子go程
	for i := 0; i < 3; i++ {
		go display(i)
	}

	//go func() {
	//	count := 1
	//	for {
	//		fmt.Println("=============> 这是子go程:", count)
	//		count++
	//		time.Sleep(1 * time.Second)
	//	}
	//}()

	//主go程
	count := 1
	for {
		fmt.Println("这是主go程:", count)
		count++
		time.Sleep(1 * time.Second)
	}
}

执行以上代码,你会看到主go程序和子go程的输出是没有固定先后顺序。因为它们是两个 goroutine 在执行(启动多个字go程,他们会竞争cpu资源)。

提前退出

package main

import (
	"fmt"
	"runtime"
	"time"
)

//return  ===> 返回当前函数
//exit ===> 退出当前进程
//GOEXIT ===> 提前退出当前go程

func main() {
	go func() {
		go func() {
			func() {
				fmt.Println("这是子go程内部的函数!")
				//return //这是返回当前函数
				//os.Exit(-1) //退出进程
				runtime.Goexit() //退出子当前go程
			}()

			fmt.Println("子go程结束!") //这句会打印吗? 会1:  不打印2
			fmt.Println("go 2222222222 ")

		}()
		time.Sleep(2 * time.Second)
		fmt.Println("go 111111111111111")
	}()

	fmt.Println("这是主go程!")
	time.Sleep(3 * time.Second)
	fmt.Println("OVER!")
}

2、通道(channel)

通道(channel)是用来传递数据的一个数据结构。

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

ch := make(chan int)

//传递值
ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据,并把值赋给 v

1)无缓冲通道

package main

import "fmt"

func main() {
	//此时是无缓冲的管道
	numChan := make(chan int)
	go func() {
		for i:=0; i<50; i++ {
			data:= <- numChan
			fmt.Println("子go程1 读取数据  ===》 data:",data)
		}
	}()

	for i:=0; i<50; i++ {
		numChan <- i
		fmt.Println("子go程2 写入数据:",i)
	}
}

2)有缓冲通道

package main

import (
	"fmt"
	"time"
)

func main() {
	//numsChan := make(chan int, 10)
	//1. 当缓冲写满的时候,写阻塞,当被读取后,再恢复写入
	//2. 当缓冲区读取完毕,读阻塞
	//3. 如果管道没有使用make分配空间,那么管道默认是nil的,读取、写入都会阻塞
	//4. 对于一个管道,读与写的次数,必须对等

	var names chan string //默认是nil的
	names = make(chan string, 10)

	go func() {
		fmt.Println("names:", <-names)
	}()

	names <- "hello" //由于names是nil的,写操作会阻塞在这里
	time.Sleep(1 * time.Second)

	numsChan1 := make(chan int, 10)

	//写
	go func() {
		for i := 0; i < 50; i++ {
			numsChan1 <- i
			fmt.Println("写入数据:", i)
		}
	}()

	//读
	go func() {
		for i := 0; i < 50; i++ {
			fmt.Println("主程序准备读取数据.....")
			data := <-numsChan1
			fmt.Println("读取数据:", data)
		}
	}()

	for {
		fmt.Println("这是主go程,正在死循环")
		time.Sleep(1 * time.Second)
	}
}

当管道的读写次数不一致的时候

  1. 如果阻塞在主go程,那么程序会崩溃
  2. 如果阻塞在子go程,那么会出现内存泄露

3)for range读取管道

package main

import "fmt"

func main() {

	numsChan2 := make(chan int, 10)

	//写
	go func() {
		for i := 0; i < 50; i++ {
			numsChan2 <- i
			fmt.Println("写入数据:", i)
		}
		fmt.Println("数据全部写完毕,准备关闭管道!")
		close(numsChan2)
	}()

	//遍历管道时,只返回一个值
	//for range是不知道管道是否已经写完了,所以会一直在这里等待
	//在写入端,将管道关闭,for range遍历关闭的管道时,会退出
	for v := range numsChan2 {
		fmt.Println("读取数据 :", v)
	}

	fmt.Println("OVER!")
}

4)判断通道关闭

package main

import "fmt"

func main() {
	numChan := make(chan int, 10)

	//写
	go func() {
		for i := 0; i < 10; i++ {
			numChan <- i
			fmt.Println("写入数据:", i)
		}

		close(numChan)
	}()

	for {
		v, ok := <-numChan // ok-idom模式判断
		if !ok {
			fmt.Println("管道已经关闭了,准备退出!")
			break
		}

		fmt.Println("v:", v)
	}

	fmt.Println("OVER!")
}

5)单向通道

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

克里斯蒂亚诺·罗纳尔达

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值