GoLang基础

GoLang基础

一. 环境部署

1.1 源码包下载

  1. 国外网址:https://golang.org/dl
  2. 国内镜像:https://golang.google.cn/dl
  3. 中文网址:https://studygolang.com/dl

1.2 在Linux中部署

# 解压源码包
sudo tar -zxvf goxxxlinux-amd64.tar.gz -C /usr/local

# 配置环境变量
sudo vim ~/.bashrc
export GOROOT=/usr/local/go # go的源码包所在的位置
export GOPATH=$HOME/go # 开发者Go的项目的默认目录-目前基本废弃
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin 
# 加载
source ~/.bashrc

# 检测
go version
go help

1.3 IDE推荐

  1. Goland:最好的但是收费;
  2. VsCode:轻量的但需要额外组件但免费;
  3. Vim + Go插件:大佬专用;

二. 语言特性

2.1 优势

  1. 极简单的部署方式,可以直接编译成机器码后,直接二进制部署即可;
  2. 是静态类型语言,编译时即可检查出隐藏的大多数问题;
  3. 语言层面的并发,切换成本更低;
  4. 包含强大的runtime系统调度机制,高效的GC垃圾回收机制,且拥有丰富的标准库;
  5. 简单易学,仅有25个关键字;
  6. C语言简介基因,内嵌C语法支持;

2.2 强项

  1. 云计算基础设施:docker、kubernetes、etcd、consul、cloudflare CDN、七牛云存储;
  2. 基础后端软件:tidb、influxdb、cockroachdb;
  3. 微服务:go-kit、micro、monzo bank、typhon、bilibili;
  4. 互联网基础设施:以太坊、htperledger;

2.3 缺点

  1. 大部分三方包在github,缺乏权威的管理;
  2. 无泛化类型,在1.18中已支持泛型;

三. 基础语法

3.1 HelloWorld

package main // 生命当前文件所属包,同一目录下的文件,必须属于同一包

import "fmt" // 导入包

func main(){ // 主函数入口 && 函数名和'{'必须在同一行
  fmt.println("Hello golang, hello world.") // 语句也可以加分号
}

3.2 变量的声明

package main

import "fmt"

// 全局单变量的定义方式
var a int 
var b = 100
var c int = 20

// 多变量的定义
var (
	d string
	e = "hello vars."
)
var f, g int = 101, 102 // 类型可以取消(基于值的推断)

func main(){
	fmt.Println("全局变量全定义:", a, b, c, d, e)

	var g string  = "1a" // 所有全部变量的定义,均可用于局部变量
	h := 100 // 仅可以用于局部变量的定义
	fmt.Println("局部变量全定义:", g, h)
}

3.3 常量的声明

package main

import "fmt"

const A int = 100 // 因为是常量所以声明必须带值
const B = "World." // 推断
const ( // 多声明 1
	C = "200"
	D string = "Alvin"
)
const E, F = 101, "Bruce" // 多声明 2 - 但是建议使用 1 

const ( // 利用 iota 关键字
	AA = iota * 10
	BB
	CC
	DD
)

func main(){
	fmt.Println(A,B,C,D,E,F)
	fmt.Println(AA,BB,CC,DD)
}

3.4 基础数据类型

整型

package main

import "fmt"

func main(){
	var (
		a int // 根据系统的为数决定长度,32位等同int32, 64位等同int64
		b int8 // 1byte, 最高位0/1表示符号
		c int16 // 2bytes, 最高位0/1表示符号
		d int32 // 4bytes,最高位0/1表示符号
		e int64 // 8bytes,最高位0、1表示符号
		f uint // 根据系统的为数决定长度,32位等同int32, 64位等同int64
		g uint8 // 1byte, 无符号
		h uint16 // 2 bytes,无符号
		i uint32 // 4 bytes,无符号
		j uint64 // 8 bytes,无符号
	)
	a,b,c,d,e = 012, -127, 0x1a, 0x1a, 300
	// 占位符,b-二进制,o-八进制,x/X-十六进制,d-十进制
	fmt.Printf("8:%o, 10有符号:%d, 16小a:%x, 16大a:%X, 2有符号:%b \n", a,b,c,d,e)

	f,g,h,i,j = 012, 255, 0x1a, 0x1a, 300
	fmt.Printf("8:%o, 10无符号:%d, 16小a:%x, 16大a:%X, 2无符号:%b \n", f,g,h,i,j)
}

整型衍生

/* 
byte 1字节存储,类型衍生自 uint8, 所以 byte 可以和 uint8 string 等类型转换(但不适合处理中文)
*/
b := 'A' // 单引号时表示 byte
s := "A" // 双引号时表示 string

/*
rune 4字节存储,类型衍生自 int32,对含有中文的 string 有很好的支持
*/

c := "是谁在看我呢QAQ"
fmt.Println(c, fmt.Sprintf("len: %d", len(c))) // utf-8 中文占 3 字节,英文字母占 1 字节,所以共 21
fmt.Println([]byte(c), fmt.Sprintf("len: %d", len([]byte(c)))) // string 底层以 byte  数组形式存储, 所以转换后为 21
fmt.Println([]rune(c), fmt.Sprintf("len: %d", len([]rune(c)))) // rune 用 4 字节存储 unicode, 所以所有字符转换后为 9

浮点数

package main

import "fmt"

func main(){
	var (
		a float32 // 4bytes
		b float64 // 8bytes
		c = 1.1 // 推测默认为float64(也怀疑与系统位数有关)
	)
	a = 2.1120001123123123
	b = 3.1122

	// 占位符,T-数据类型,f-无指数,e/E-指数,g/G-自动选择f或E
	fmt.Printf("%T, %e\n", c, c)
	fmt.Printf("%T, %.3f\n", b, b)
	fmt.Printf("%g, %G\n", a, b)
}

字符串

/* 
string 默认在堆中分配内存存储
string 在内存中是一个 []byte 一个 byte 的切片
*/
package main

import (
	"fmt"
	"strings"
)

func main(){
	// 双引号与反引号
	s1 := "aa\nbb" // 双引号中 '\' 有转义作用
	s2 := `aa\nbb` // 反引号中 '\' 无转义作用

	// 获取字符串长度
	fmt.Println(len(s1), len(s2))
	
	// 3种字符串拼接
	s1 = s1 + "aaaa"
	s2 = fmt.Sprintf("%s%s", s2, "aaa")
	s3 := strings.Join([]string{"aa", "BB"}, "-")
	fmt.Println(s1, s2, s3)
	// 字符串分割
	fmt.Println(strings.Split(s2, `\n`))
	// 判断字符串是否包含指定字符串
	fmt.Println(strings.Contains("abcde", "bcd"))
	// 判断字符串前缀是否匹配
	fmt.Println(strings.HasPrefix("abc", "ab"))
	// 判断字符串后缀是否匹配
	fmt.Println(strings.HasSuffix("abc", "bc"))

}

3.5 扩展数据类型

数组

  1. 是一组相同唯一类型且长度固定的数据项序列,唯一类型可为原始类型或自定义类型,值类型
/* 声明 */
var arr1 [4]int
arr1 = [4]int{1,2,3,4}
arr2 := [...] int {1,2,3,4}

/* 遍历 */
arr1 := [...]int{1,2,3,4,5,6}
for index, value := range arr1{
	fmt.Println(index, value)
}
for i:=0;i<len(arr1);i++{
	fmt.Println(i, arr1[i])
}

/* 排序 */
func SortArr(arr []int){ // 这里不要使用 array 的指针,建议使用 slice
	tag := 0
	for j:=0;j < len(arr) - 1; j++{
		if arr[j] > arr[j + 1]{
			arr[j], arr[j + 1] = arr[j +1] , arr[j]
			tag += 1
		}
	}
	if tag > 0{
		SortArr(arr)
	}
}
  1. 数组是在内存中开辟一块固定大小的连续内存空间,数组的地址为第一个元素的内存地址;
	arr1 := [4]int{1,2,3,4}
	fmt.Printf("%p %p %p %p", &arr1[0], &arr1[1], &arr1[2], &arr1[3])  //0x140000b4000 0x140000b4008 0x140000b4010 0x140000b4018
  1. 多维数组
/* 声明 */
var two [3][3]int
two = [3][3]int{
	{1,2,3},
	{4,5,6},
	{7,8,9},
}

/* 遍历 */
for _, arr := range two{
	for _, val := range arr{
		fmt.Println(val)
	}
}

Slice

  1. 动态数组可以自动扩容,引用类型;
/* 声明 */
var s1 []type = make([]type, len, cap)
var s1 []type = {1,2,3}
var s1 []type = arr[startIndex:endIndex] // slice 可以是 arr 的切片,类似Python的切片
  1. slice没有自己的任何数据,它是底层数组的一个抽象
/* slice本质是一个 struct 用于抽象底层 array */
type slice struct {
    array unsafe.Pointer // 指向底层数组
    len int // 长度
    cap int // 容量
}

/* slice 与 array 之前的联系与转换 */
package main

import "fmt"

func main(){
	var s1 []int
	a1 := [5]int{1,2,3,4,5} // 数组
	s1 = a1[:2] // 切片后 len=2, cap=5
	fmt.Println(cap(s1), len(s1), s1) // 5 2 [1, 2]
	s1 = append(s1, 0, 1, 2, 3, 4, 5) // append 3次时 len = cap ,之后扩容会从新开辟更大的空间(扩容一个 cap)
	fmt.Println(cap(s1), len(s1), s1) // 10 8 [1 2 0 1 2 3 4 5]
	fmt.Println(a1) // [1 2 0 1 2] 在扩容前受到 slice 的修改影响,扩容后 slice 与原 arr 没有联系了
}
  1. Copy 函数从 src 中复制元素到目标 dst 中,并返回成功的数量, copy 的两个 slice 没有联系;
var s1 = []int{1,2,3}
var s2 = []int{100,200}
fmt.Println(copy(s1,s2)) // 2
fmt.Println(s1) // [100, 200, 3]
  1. append 函数向 slice 中追加一个或多个元素,然后返回一个修改后的 slice;
s1 = append(s1, 1,2,3,4)

Map

  1. 一种无序、长度不固定的键值对的集合,引用类型;
/* 
1. 声明变量,此时的 map 为指向 nil 的 map 指针, 无法向 nil 插入数据
2. 使用 make 在内存中创造 map 的实际数据结构,并使用 map_variable 指向这个数据结构
*/
var map_variable map[key_data_type]value_data_type
map_variable = make(map[key_data_type]value_data_type)

/* 直接创建  */
rating := map[string]float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 }

/* 插入数据  */
rating["Rust"] = 4.8

/* 遍历-range出来的是 key */
for language := range rating {
  fmt.println(language, rating[language])
}

/* 取值  */
grade = rating["Rust"] // key 为空不会报错,会返回空值
grade, ok := rating["C"] // ok 为 bool 类型反馈是否取值成功

/* map 长度  */
mapLen := len(rating) // map key 的数量

/* 
map 删除  
1. 这种删除后,key 会被标记为空,但是key仍然存在,防止同样的 key 再次插入
2. 如果需要完全释放,可以考虑 从建 map ,或使用 go-zero 开发的 safemap
*/
delete(rating, "C++")
  1. map 不能使用 == 操作符与其他 map 进行比较,可以与 nil 比较(map是否为空);

  2. map 的底层实现

image-20220419122105765

Struct

  1. 是由一系列具有相同或不同类型的数据构成的集合;
// 定义struct
type structVariableType struct {
		member definition
 		...
  	member definition
}

// 实例化
varName1 := structVariableType{Val1, Val2, Val3}
varName2 := structVariableType{key1:Val1, key2:Val2, key3:Val3}
  1. 结构体的标签 Tag :一个附属于字段的字符串,会在编译阶段关联到成员的元信息中,在运行时可以通过反射 reflect 机制读取,在 json 和 beego 等包中有使用;
// 格式必须要各遵守,格式错误不会在编译和运行时报错
`key1:"value1" key2:"value2" key3:"value3"`

// 常用的 Tag-key 有 json、orm、form 等
type TagStruct struct {
  Name string `json:"JSON标签" orm:"Beego标签" gorm:"GORM标签" bson:"MongoDB标签" form:"表单标签" binding:"表单验证标签"`
}

// 在 json 中使用, 并通过 reflect 取出 Tag
package main

import (
	"encoding/json"
	"fmt"
	"reflect"
)

func main(){
	type User struct {
		UserId int `json:"user_id" bson:"user-id"`
		UserName string `json:"user_name" bson:"user-name"`
	}

	u1 := &User{UserId: 1, UserName: "Bruce"}
	
	// 用于 json 的序列化
	j, _ := json.Marshal(u1)
	fmt.Println(string(j)) // {"user_id":1,"user_name":"Bruce"}

	// 通过 reflect 取出
	t := reflect.TypeOf(u1)
	field := t.Elem().Field(0)
	fmt.Println(field.Tag.Get("json"), field.Tag.Get("bson")) // user_id user-id
}
  1. struct 的内存布局:struct 在内存中是一段连续的内存空间;CPU 在访问内存时, 每次读取一个 word(在64位中为8bytes, 32位中为4bytes),为尽量避免同一属性两次读取,go 引入了对齐的概念;
// 在 struct 中每个属性均拥有对齐系数,使用 unsafe.Alignof(attr) 获取
unsafe.Alignof(struct.attr)

`
对齐规则:
- atrr 为任意类型:对齐系数至少为 1 ;
- attr 为 struct 类型:计算 strct 的每个 attr 的对齐系数,取最大值, 对于 map(实际为一个指针) 为 8 ;
- attr 为 array 类型:为构成数组元素类型的对齐系数了,对于 slice 为 8 ;
- 连续的较小大小的 attr 可以合并;
`

type Size12 struct {
  A int8
  B int32
  C int8
}
s12 := Size12{}
unsafe.Size(s12) // 12

type Size8 struct {
  A int8
  C int8
  B int32
}
s8 := Size8{}
unsafe.Size(s8) // 8
  1. 嵌套
type Human struct {
  name string
  age int
}

/* 
匿名字段:实际就是字段的继承,可以理解为字段名和字段类型相同
*/
type Student struct {
  Human // 匿名字段, Student 包含所有的 Human 字段
  speciality string
  int // 匿名字段,也可以为非 struct 类型
}

alvin := Student{Human{"Burce", 28, 160}, "Computer Science", 110}

/*
字段提升:在匿名的情况下,可以省略字段名(匿名字段的类型)访问字段;
1. 如果与外层字段名相同,则使用就近原则;
*/
alvin.Human.name 
alvin.name

  1. 比较
/*
当结构体内的所有字段都是可比较类型时,Struct 可以比较,否则不行;
*/

指针

  1. 指针是存储另一个变量的内存地址的变量;
var p *int  // 声明一个指向int类型的指针
p = &a // 获取变量的地址并赋值于指定变量的指针
var c int = *p // 解引用
var p2 **int // 声明一个指向int类型的二级指针
p2 = &p // 二级指针是指向一级指针的指针
  1. array/slice 的指针与引用顺序
arr1 = [4]int{1,2,3,4}
p := &arr1
n1 := (*p)[0] // [0] 的优先级高于 *p
  1. 经典换值例子
package main

import "fmt"

func changeAB(a,b *int){
	temp := *a // 从指针 a 解出 int 的值并 copy 给中间变量 temp
	*a = *b // 用指针 b 指向空间的值替代指针 a 指向的区域的值
	*b = temp // 用 temp 覆盖指针 b 指向的空间的值
}

func main(){
	n1 := 100
	n2 := 200
	changeAB(&n1, &n2)
	fmt.Println(n1, n2)
}
  1. 正常指针、unsafe.Pointer、uintptr 间的关系与转换
正常指针 // 常规的安全指针,指向特定的类型
unsafe.Pointer // 可以指向任何类型的指针
uintptr // 一个可以承载任何指针的无符号整型,可以进行一些运算

var n int = 100
var p1 *int = &n // 获取 n 的正常指针
var up1 = unsafe.Pointer(p1) // 将 *int 转换为 unsafe.Pointer 
var pint = uintptr(up1) // 将 unsafe.Pointer 转换为 uintptr 类型
var p2 = (*int)(up1) // 将 unsafe.Pointer 转换回 *int

Tips: Make 与 New

Make(Type, args) 函数:用于内建类型 (map、slice、channel)的内存分配,并填充适当的值(非 0 值),返回 Type;

New(Type) 函数:用于各种类型的内存分配,并填充 0 值,返回 *Type;

3.6 运算符

算数运算符

+ - * / % ++ --

关系运算符

== != > < >= <=

逻辑运算符

&& || !

位运算符

&   - 与运算
|   - 或运算
^   - 异或
<<  - 左移
>>  - 右移
&^  - 掩码

/* 举例 */
func main(){
	a := 0xA
	b := 0x7
	PrintB(a) // 1010
	PrintB(b) // 0111
	PrintB(a & b) // 0010
	PrintB(a | b) // 1111
	PrintB(a ^ b) // 1101
	PrintB(a &^ b) // 1000
	PrintB(a >> 1) // 0101
	PrintB(b << 1) // 1110
}

func PrintB(n int){
	fmt.Printf("%.4b ", n)
}

赋值运算符

= += -= *= /= %= <<= >>= &= |= ^=

单目运算符

! *(/指针) &(取指针) ++ -- +(正号) -(负号)

后缀运算符

() [] ->

特殊运算符

_ - 空白符,可以代替任何类型

3.7 输入输出

输出 Print

func Print(a ...interface{})(n int, err error)
func Println(a ...interface{})(n int, err error)
func Printf(format string, a ...interface{})(n int, err error)

/*  
常用占位符 
*/
%v - 输出所有val,变种有 %+v(带 key) %#v(带 Type Key)
%T - 输出 Type
%t - 输出 Bool
%s - 输出 String
%f - 输出 float
%d - 输出 十进制 int
%b - 输出 二进制 int
%o - 输出 八进制 int
%x - 输出 16进制 int(字母小写)
%X - 输出 16进制 int (字母大写)
%c - 输入 int decode Unicode 后的字符
%p - 输出 指针

输入 Scan

var x int
var y float64
fmt.Scanln(&x,&y) // 接收键盘输入 100 2.12
fmt.Scanf("%d,%f", &x, &y) // 根据特定格式接收,此处为 100,2.12

输入 bufio

/* 
bufio 包中都是IO操作的方法
*/
reader := bufio.NewReader(os.stdin)
s1, _ := reader.ReadString("\n")
fmt.Println("读到的数据: ", s1)

3.8 分支语句

if 语句

if [statement;] condition { // 在 statement 定义的变量仅在 if 代码块中使用
  
} else if condition {
  
} else {
  
}

switch 语句

switch [statement;][varable] {
    case cons1: // 如果 varable 存在则为 varable 与 cons1 的比较结果判断是否成立
    	...
    case cons2: // 如果 varable 不存在,在根据 case 的表达本身是否成立判定
    	...
  		fallthrough // 执行下一个 case 中的语句,不关系 case 的条件是否成立
    default: // default 无论放在什么位置都会最后执行
    	...
}

select 语句

/* 
select 仅用于 channel 对象的读写,且当多个 case 同时满足时,会按照一种伪随机机制选择其中一个满足条件的分支
If multiple cases can proceed, a uniform pseudo-random choice is made to decide which single communication will execute.
*/
select {
		case msg1 := <-c1:
  		...
  	case msg2 := <-c2:
  		...
}

3.9 循环语句

for 语句

for [init;] [condition;] [post] { // init 中的变量仅可以在循环体内使用
  continue // 退出本次循环
  break // 退出整个循环
}

goto 语句

goto label;
..
..
label: statement;
/* 在多层循环中使用 */
/* 在统一错误代码中使用 */
    if err != nil {
        goto onExit
    }
    err = secondCheckError()
    if err != nil {
        goto onExit
    }
    fmt.Println("done")
    return
onExit:
    fmt.Println(err)
    exitProcess()

3.10 函数

基本定义

func funcName(parametername type1, parametername type2) (output1 type1, output2 type2) {
...
return value1, value2 
}

参数

/* 可变参数 */
func aa(args ...int) // args 为 []int

/*
值传递:实参将值传递给形参之后,实参和形参不再有联系;
指针传递:形参接收的是一个指针变量,这个变量指向某个 Obj,所以在函数内部对 Obj 在函数外有效;
注意:map 类型为指针类型引用,slice 虽然也为指针类型引用,但在 slice 发生扩容时,slice 的元数据空间需要变更,实参和形参会出现不一致;
*/
func bb(mp map) {}
func cc( ptr *int) {}

返回值

func aa() (int, int){ // 无名返回值
  var a, b int
  return a, b
}

func bb() (a int, b int){ // 有名返回值
  return
}

**defer 与 return **

/*
defer : 后面跟 func,该 func 会在函数执行完毕后 按照压栈顺序,倒序执行;
return: 返回函数的返回值;

defer 与 return 的执行顺序关系,依据函数返回值的类型不同,分为两种:
	- 无名返回值:创建了一个临时零值变量(假设为 s)作为返回值 
							=> 将返回值赋值给 s 
							=> 执行所有的 defer 函数 
							=> 将 s return 到调用出
	- 有名返回值:返回值为函数头中定义的 i
							=> 执行所有的 defer 函数
							=> 将 i return 到调用处
*/
func f1() int { // 无名返回值
	n := 100
	defer func(np *int){*np++}(&n)
	return n
}

func f2() (n int) { // 有名返回值
	n = 100
	defer func(np *int){*np++}(&n)
	return
}

func main(){
	fmt.Println(f1()) // 100
	fmt.Println(f2()) // 101
}

函数变量

/* 
在 Go 中无法在一个函数内部再声明一个函数,所以所有的函数都在顶层声明的;
但匿名函数 + 函数变量可以在函数内部声明并赋值,同时也可以作为返回值;
*/

/* 闭包:函数拥有并返回内层函数,内层函数调用函数的 某些局部属性; */

func func1() func() int { // 无名返回值
	n := 0 // 在闭包中局部变量的生命周期改变,在 func1 执行完毕后不会释放;
	add := func() int {
		n ++
		return n
	}
	return add
}

func main(){
	add := func1()
	fmt.Println(add()) // 1
	fmt.Println(add()) // 2
	fmt.Println(add()) // 3
}

/* 回调函数:callback,如 func2 作为 func1 的参数,那么 func2 就是回调函数,func1 叫高阶函数;*/

func add(a, b int) int { // 回调函数1
	return a + b
}
func sub(a, b int) int { // 回调函数2
	return a - b
}

func oper(a, b int, fun func(int, int) int) int { // 高阶函数
	return fun(a, b)
}

func main() {
	fmt.Println(oper(3, 1, add)) // 4
	fmt.Println(oper(3, 1, sub)) // 2
}

init 方法 与 import

image-20220419205050710

3.11 方法

  1. 方法是一个带有特殊接收器类型的函数,方法通过接收器接收到对象后,可以在方法内部访问;

Go 不是纯粹的面向对象编程语言,它不支持类,因此类型的方法时一种实现类似于类的行为的方法;

/* 定义 */
func (t Type) methodName(parameter list)(return list) { // 有名字的定义方式
...
}
func (t Type) (parameter list)(return list) { // 匿名定义方式
...
}
  1. 方法的值传递与指针传递;
/* 
指针传递与值传递的区别在接收器接收到的对象是指针还是对象:
*/
func (ptr *Type) methodName(parameter list) // 此为指针传递
func (T Type) methodName(parameter list) // 此为值传递

/* 
方法依据接收者与 Type 关联:
1. 当接收者为 (T Type) 时,方法关联到 Type 和 *Type;
2. 当接收着为 (PTR *Type) 时,方法关联到 *Type;
3. 这可以用来解释,为什么明明实现了 interface 的某个方法,但是就是无法指向的问题;

为什么无论值传递还是指针传递,都可以直接调用属性成功呢?
1. GoLang 语法糖:在使用选择器(Selector)调用方法时,编译器会自动做好取指针/值的操作;
*/

type StructA struct {}

func (s StructA) ValueReceiver () {}

func (s *StructA) PointReceiver () {}

func main() {
  value := StructA{}
  point := &value
  // 编译器不做处理,就是value.ValueReceiver()
  value.ValueReceiver()
  
  // 其实是(&value).ValueReceiver()的简便写法 - 语法糖
  value.PointReceiver()
  
  // 其实是(*point).ValueReceiver()的简便写法 - 语法糖
  point.ValueReceiver()
  
  // 编译器不做处理,就是point.ValueReceiver() 
  point.PointReceiver()
}

四. 重点基础

4.1 import 导包

import "os" // 导入的包必须使用
import _ "os" // 如果不想使用,可以用 _ , 此时会调用包的 init 函数并初始化一下全局变量
import . "os" // 不加前缀的调用
import oss "os" // 别名,可以使用别名来调用

4.2 面向对象

type Human struct {
	Name string // 封装,大写开头的属性,可以通过对象直接访问
	age int8 // 封装,小写开头的属性,无法通过对象直接访问
}

func (h *Human) GrowOld(){
	h.age ++
}

func (h *Human) TellAge(){
	fmt.Printf("Human %s's age is %d. \n", h.Name, h.age)
}

type Student struct {
	Human // 继承:匿名的 Human 属性,将继承所有属性与方法;
	teacher Human // 组合:有名的 teacher 字段
	grade int8
}

func (s *Student) TellAge(){ // 重写
	s.Human.TellAge()
	fmt.Printf("%d Grade student %s's age is %d. \n", s.grade, s.Name, s.age)
}

func main(){
	Alvin := Human{"Alvin", 20}
	Alvin.GrowOld()
	Alvin.TellAge()

	Bruce := Student{Human{"Bruce", 30}, Human{"Diana", 22},3}
	Bruce.GrowOld()
	Bruce.TellAge()
}

4.3 接口

  1. If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
/*
接口定义对象的行为
1. 在 Go 接口定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了该接口;
2. 接口把所有具有共性的方法定义在一起;
3. interface 本质是一种类型,一种指针类型;
4. interface 是为了实现多态的功能;
5. 既然类实现了接口的所有方法,那接口当然可以直接指向某个类的实例,并通过这个接口调用这个实例下的所有这个接口的方法;
6. Go 的编译器检测接口与类的关系,并将接口与实现它的类做了关联;
*/

package main

import (
	"fmt"
)

type notifier interface {
	notify(msg string) // 函数性 + 参数 = 函数的签名
}

type User struct {
	Name  string
	Email string
}

func (u *User) notify (msg string){
	fmt.Printf("给用户%s的邮箱%s发送消息:%s\n", u.Name, u.Email, msg)
}

type Admin struct {
	Name  string
	Email string
}

func (a *Admin) notify (msg string){
	fmt.Printf("给管理员%s的邮箱%s发送消息:%s\n", a.Name, a.Email, msg)
}

func sendNotification(n notifier, msg string){
	n.notify(msg)
}

func main(){
	var n notifier // 接口为指针类型
	a := new(Admin)
	a.Name = "Bruce"
	a.Email = "Bruce@go.io"
	u := User{"Alvin", "Alvin@go.io"}
	n = a
	n.notify("可以直接使用接口变量指向实现他的类的对象。")
	n = &u  // 当对象的方法为值传递时,可以不给指针: n = u ,但如果是引用传递,则必须写成:n = &u !!!!
	n.notify("可以直接使用接口变量指向实现他的类的对象。")
	sendNotification(a, "也可以在函数的形参中使用,用来接收实现该接口的对象,同时能调用该接口的方法。")
	sendNotification(&u, "也可以在函数的形参中使用,用来接收实现该接口的对象,同时能调用该接口的方法。")
}
  1. 接口断言:Go中任何类型都实现了 interface{} 接口,所以 interface{} 可以用来指向任意一个类型,如果要恢复真实类型,需要用到断言;
package main

import (
	"fmt"
)

type notifier interface {
	notify(msg string) // 函数性 + 参数 = 函数的签名
}

type User struct {
	Name  string
	Email string
}

func (u User) notify (msg string){
	fmt.Printf("给用户%s的邮箱%s发送消息:%s\n", u.Name, u.Email, msg)
}

type Admin struct {
	Name  string
	Email string
}

func (a *Admin) notify (msg string){
	fmt.Printf("给管理员%s的邮箱%s发送消息:%s\n", a.Name, a.Email, msg)
}

func main(){
	var n1 notifier
	a := new(Admin)
	u := User{"Alvin", "Alvin@go.io"}

	n1 = a
	arr := n1.(*Admin) // 第一种:断言不通过会panic
	ar, ok := n1.(*Admin) // 第二种:断言不通过不会panic
	fmt.Println(arr, ok, ar)

	n1 = u

	switch n1.(type) { // 第三种:基于switch的断言
	/*
		这个证实了当接收器为 (ptr *Type) 时,方法只与 *Type 关联,所以无法断言 Admin 类型;
		而当接收器为 (T Type)时,方法却是同和 Type 与 *Type 关联,所以可以断言 User 和 *User 类型;
	*/
	case *Admin:
		fmt.Println("是 *Admin 类型的对象。")
	case User:
		fmt.Println("是 User 类型的对象。")
	case *User:
		fmt.Println("是 *User 类型的对象。")
	default:
		fmt.Println("不是以上任何类型的对象。")
	}

}
  1. Type 关键字
/* 定义结构体 */
type person struct {
  name string
  age int
}

/* 定义接口 */
type USB interface {
  start()
  end()
}

/* 定义新类型 */
type myint int
func (m *myint) add(){
	*m = *m + 1
}
var m myint = 100
var n int = 100
m = n // cannot use n (type int) as type myint in assignment

/* 定义函数类型 */
type myfun func(int,int)(string)

/* 
定义类型别名,主要用于解决代码升级、迁移中的兼容性问题 
TypeAlias 与 Type 本质仍然是同一类型
*/
type myint = int
func main(){
	var m myint = 100
	var n int = 100
	m = n // 不会报错,但是也不可定义方法了,除非将别名写到本名所在的包中;
}

4.4 错误与异常

  1. 错误是可能出现问题的地方出现问题,比如打开文件失败,在预料之中;

不要忽略任何一个 Error;

/* 简单的 error 使用举例 */
func main() {  
    f, err := os.Open("/test.txt")
    if err != nil {
        fmt.Println(err) // 打印 err.msg: open /test.txt: No such file or directory  
        return
    }
  //根据 f 进行文件的读或写
    fmt.Println(f.Name(), "opened successfully") 
}

/* 
error 的接口,任何一个实现该接口的类型都能够当做 error 使用;
*/
type error interface {
    Error() string
}

/* 从错误中提取更多信息的方法1:断言底层结构类型并从结构字段获取更多信息 */
func main() {  
    f, err := os.Open("/test.txt")
    if err, ok := err.(*os.PathError); ok { // 断言为 PathError 类型后调用其属性;
        fmt.Println("File at path", err.Path, "failed to open") // File at path /test.txt failed to open  
        return
    }
    fmt.Println(f.Name(), "opened successfully") 
}

/* 从错误中提取更多信息的方法2:断言底层结构类型,并使用方法获取更多信息 */
func main() {  
    addr, err := net.LookupHost("golangbot123.com")
    if err, ok := err.(*net.DNSError); ok {
        if err.Timeout() {
            fmt.Println("operation timed out")
        } else if err.Temporary() {
            fmt.Println("temporary error")
        } else {
            fmt.Println("generic error: ", err)
        }
        return
    }
    fmt.Println(addr) // generic error:  lookup golangbot123.com: no such host  
}

/* 从错误中提取更多信息的方法3:直接比较 */
var ErrBadPattern = errors.New("syntax error in pattern")   // 在 filepath 包中定义了 ErrBadPattern

func main() {  
    files, error := filepath.Glob("[")
    if error != nil && error == filepath.ErrBadPattern {
        fmt.Println(error) // syntax error in pattern 
        return
    }
    fmt.Println("matched files", files)
}

/* 自定义异常1:使用 errors 包下的 New() 函数和 fmt 包下的 Errorf() 函数 */
// errors 包:
package errors
func New(text string) error {
     eturn &errorString{text}
} 
type errorString struct {
      s string
}
func (e *errorString) Error() string {
     return e.s
}

//fmt 包:
func Errorf(format string, a ...interface{}) error {}

/* 自定义异常2:独立实现 error 接口 */
type areaError struct {  
    err    string
    radius float64
}

func (e *areaError) Error() string {  
    return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}

if err, ok := err.(*areaError); ok  // 在使用时,可以使用 error 的额外异常提取的几条策略;
  1. 异常是不应该出现问题的地方出现问题,比如空指针,在预料之外;
/* 
Golang 内置函数 panic 与 recover 来触发和终止异常处理流程,同时使用 defer 来延迟执行收尾函数;
1. 异常可由引用空指针、下标越界或显示调用 panic 产生并从调用栈传递,直至所有协程均停止工作,程序退出;
2. 在函数中异常发生后,函数执行结束,但仍然会调用相应的 defer 延迟函数;
3. 所以可在 defer 函数中使用 recover 函数来终止异常的传递;
*/

/* 
异常处理的作用域,一下场景建议按照异常来处理,防止出现一切皆错误的情况:
1. 空指针引用
2. 下标越界
3. 除数为0
4. 不应该出现的分支
5. 实参必定不能有误的函数
*/
  1. 错误是业务的一部分,而异常不是;
  2. 错误处理的正确姿势:
/* 
1. 失败原因只有一个时,不使用 error,可以使用 bool;
2. 函数内语句执行必定成功,不会失败时,不返回 error;
3. 函数返回 error 时,应该放在返回值的最后一位( bool 也建议这样做);
4. Error 对象集中定义,方便复用与修改,防止同一错误出现多个版本的 Error 对象;
5. Error 层层传递,层层加日志,方便排查;
6. Error 发生时,使用 defer 来做必要资源释放或其他收尾工作;
7. 当 Error 是偶然发生时,可以多尝试几次,不立即返回错误;
8. 当上层函数不关系 Error 时,建议不要返回,比如 delete/clear 类的资源释放;
9. 当错误发生时,不要忽略其返回值,可能含有写有意义的数据;
*/
  1. 异常处理的正确姿势:
/*
1. 在开发阶段,坚持速错,多调用 painc 来快速定位问题;
2. 在程序上生产后,应恢复异常避免程序终止;
3. 对不应该出现的分支,使用异常处理;
4. 要求入参必定没有问题的函数,使用 painc 处理;
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值