Golang快速入门学习笔记

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/HD2killers/article/details/83994089

Golang快速入门学习笔记

安装

# 我是使用的linux mint,直接通过包管理工具安装
sudo apt-get install golang
# 安装之后直接查看GOPATH的路径,这个是最主要的
go env
# 如果是空,直接编辑/etc/profile,跟上自己需要的目录
export GOPATH="/home/jcleng/go"
# 其次为了go编译的二进制文件能直接到终端执行,也可以直接
export PATH=$PATH:/home/jcleng/go/bin

hello world

# 一个项目只有一个package为main的包,func为mian的方法,可以理解为程序的入口文件
# import关键字导入包fmt,fmt包是内置,Println方法使用.连接调用
package main
import "fmt"
func main() {
    fmt.Println("Hello World")
}

变量&&常量

# 变量使用 var 关键字
var age int // 变量声明
age = 29 // 赋值

var age int = 29 // 声明变量并初始化
# 简短声明使用了 := 操作符
# 注意,简短声明要求:
	:= 操作符左边的所有变量都有初始值
	:= 操作符的左边至少有一个变量是尚未声明的
name, age := "naveen", 29 // 简短声明
num := int(5) // 增加类型

# 常量使用const,常量的值会在编译的时候确定。因为函数调用发生在运行时,所以不能将函数的返回值赋值给常量。

# type关键字类型声明
var defaultName = "Sam" // 允许
type myString string
var customName myString = "Sam" // 允许
customName = defaultName // 不允许

类型

bool
数字类型
		int8, int16, int32, int64, int // 注意:根据不同的底层平台(Underlying Platform),表示 32 或 64 位整型。除非对整型的大小有特定的需求,否则你通常应该使用 int 表示整型。
		uint8, uint16, uint32, uint64, uint
		float32, float64
		complex64, complex128
		byte // byte 是 uint8 的别名。
		rune // rune 是 int32 的别名。
string
# 测试输出
math.MaxInt32 // 大小
fmt.Printf("type of a is %T, size of a is %d", a, unsafe.Sizeof(a)) // a 的类型和大小
# 关于类型转换,只有显示转换
sum := i + int(j)

函数

# 关键词 func 开始,后面紧跟自定义的函数名 functionname (函数名)。函数的参数列表定义在 ( 和 ) 之间,返回值的类型则定义在之后的 returntype (返回值类型)处。声明一个参数的语法采用 参数名 参数类型 的方式,任意多个参数采用类似 (parameter1 type, parameter2 type) 即(参数1 参数1的类型,参数2 参数2的类型)的形式指定。之后包含在 { 和 } 之间的代码,就是函数体。
func functionname(parametername type) (returntype,returntype) {  
    // 函数体(具体实现的功能)
}
# 注意:多返回值
# 返回值必须用括号括起来:(float64, float64)
func rectProps(length, width float64)(float64, float64) {  
    var area = length * width
    var perimeter = (length + width) * 2
    return area, perimeter
}
func main() {  
    area, perimeter := rectProps(10.8, 5.6)
    fmt.Printf("Area %f Perimeter %f", area, perimeter) 
}
# 关于:命名返回值(area, perimeter float64),直接return
func rectProps(length, width float64)(area, perimeter float64) {  
    area = length * width
    perimeter = (length + width) * 2
    return // 不需要明确指定返回值,默认返回 area, perimeter 的值
}
# 机智的:空白符
# _ 在 Go 中被用作空白符,可以用作表示任何类型的任何值。
# 先后顺序还是要注意的,根据return area, perimeter先后顺序
func rectProps(length, width float64) (float64, float64) {  
    var area = length * width
    var perimeter = (length + width) * 2
    return area, perimeter
}
func main() {  
    area, _ := rectProps(10.8, 5.6) // 返回值周长被丢弃
    fmt.Printf("Area %f ", area)
}

包 package

# 所有可执行的 Go 程序都必须包含一个 main 函数。这个函数是程序运行的入口。main 函数应该放置于 main 包中,之前已经提过。
# 自定义包名称一般是父级目录的名称
# 路径
src
    geometry
        geometry.go
        rectangle
            rectprops.go
# geometry.go是main包,那么rectprops.go的包就是rectangle
package rectangle
# 关于外部调用:如 geometry.go 调用 rectprops.go 
import (  
    "fmt"
    "geometry/rectangle" // 导入自定义包,这里是相对src资源目录
)
# 只有rectprops.go文件里面的函数首字母大写才是自动导出的方法才能够使用,可以理解为大写就是public,小写就是private
func Diagonal(len, wid float64) float64 {  
    diagonal := math.Sqrt((len * len) + (wid * wid))
    return diagonal
}
# 调用
rectprops.Diagonal(2.0,2.0)

# 关于初始函数,在所有的函数之前执行,所有包都可以包含一个 init 函数。init 函数不应该有任何返回值类型和参数,在我们的代码中也不能显式地调用它。
func init() {  
}

# 包 使用空白标识符
# 全局使用
var _ = rectangle.Area // 错误屏蔽器,不调用也不会提示编译错误
# 导入包的时候使用 
import (  
    _ "geometry/rectangle" // 初始化包,但是不使用也不会提示编译错误
)

语句

# if
# 注意,条件没有括号,else 语句应该在 if 语句的大括号 } 之后的同一行中。如果不是,编译器会不通过,golang是编译器自动增加分号的
if condition {  
} else if condition {
} else {
}
# 特殊,条件可以先做运算,num作用域仅限if结构块里面
if num := 10; num % 2 == 0 { //checks if number is even

# for
# for 是 Go 语言唯一的循环语句。Go 语言中并没有其他语言比如 C 语言中的 while 和 do while 循环。
for initialisation; condition; post {  
}
# 也可以先运算
func main() {  
    for i := 1; i <= 10; i++ {
        if i > 5 {
            break //loop is terminated if i > 5
        }
        fmt.Printf("%d ", i)
    }
    fmt.Printf("\nline after for loop")
}

# switch
# case自带break,使用fallthrough不跳出
func main() {
    finger := 4
    switch finger {
    case 1:
        fmt.Println("Thumb")
    case 2:
        fmt.Println("Index")
    case 3:
        fmt.Println("Middle")
    case 4:
        fmt.Println("Ring")
    default:
        fmt.Println("Pinky")
    }
}

Arrays 数组 和 Slices 切片

# 数组的表示形式为 [n]Type,一个数组的Type全部一样
# 注意:数组的大小是类型的一部分。因此 [5]int 和 [25]int 是不同类型。数组不能调整大小,不要担心这个限制,因为 slices 的存在能解决这个问题。数组是值类型。
var a [3]int //int array with length 3
b := [3]int{12, 78, 50} // short hand declaration to create array
fmt.Println(a)
fmt.Println(b)

a := [...]string{"USA", "China", "India", "Germany", "France"}
a[0] = "Singapore"
# 关于迭代数组
a := [...]float64{67.7, 89.8, 21, 78}
for i := 0; i < len(a); i++ { // looping from 0 to the length of the array
    fmt.Printf("%d th element of a is %.2f\n", i, a[i])
}

for i, v := range a {//range returns both the index and value
    fmt.Printf("%d the element of a is %.2f\n", i, v)
    fmt.Printf("%d th element of a is %.2f\n", i, a[i])
}
# 如果你只需要值并希望忽略索引,则可以通过用 _ 空白标识符替换索引来执行。
for _, v := range a { // ignores index  
}
# 多维数组一样

# Slices 切片
# 切片是由数组建立的一种方便、灵活且功能强大的包装(Wrapper)。切片本身不拥有任何数据。它们只是对现有数组的引用。
var names []string // 值为nil的切片,“空切片”
# (names == nil) == ture

countriesCpy := make([]string, 10)
fmt.Println(countriesCpy) // 值不为nil的切片,“空切片”,这里的长度10,可以在copy切片的时候获取需要copy切片的长度
# (countriesCpy == nil) == false

a := [5]int{76, 77, 78, 79, 80}
var b []int = a[1:4] // a[start:end] 创建一个从 a 数组索引 start 开始到 end - 1 结束的切片 end>len 会编译错误
b[0] = 0
fmt.Println(a)
fmt.Println(b)
# [76 0 78 79 80]
# [0 78 79]

# {76, 77, 78, 79, 80}
#  0   1   2    3  4

# 另一种
c := []int{6, 7, 8} // 创建一个有 3 个整型元素的数组,并返回一个存储在 c 中的切片引用。
# 另一种
numa := [3]int{78, 79, 80}
nums1 := numa[:] // 创建一个一样的切片

# 切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。当多个切片共用相同的底层数组时,每个切片所做的更改将反映在数组中。
# 如上
# b[0] = 0
# [76 0 78 79 80]
# [0 78 79]

# 切片的长度和容量
fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))

# 使用 make 创建一个切片,func make([]T,len,cap)[]T 通过传递类型,长度和容量来创建切片。容量是可选参数, 默认值为切片长度,并返回引用该数组的切片
i := make([]int, 5, 5)
fmt.Println(i)

# 追加切片元素
# 说明:如果切片由数组支持,并且数组本身的长度是固定的,那么切片如何具有动态长度。以及内部发生了什么,当新的元素被添加到切片时,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回这个新数组的新切片引用。现在新切片的容量是旧切片的两倍。
# 正如我们已经知道数组的长度是固定的,它的长度不能增加。 切片是动态的,使用 append 可以将新元素追加到切片上。append 函数的定义是 func append(s[]T,x ... T)[]T
cars := []string{"Ferrari", "Honda", "Ford"}
fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars))
cars = append(cars, "Toyota")
fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars))
# 输出
# cars: [Ferrari Honda Ford] has old length 3 and capacity 3
# cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6

# ... 运算符  将一个切片添加到另一个切片(可变参数函数教程中了解有关此运算符的更多信息:printf("%d", i);)
veggies := []string{"potatoes", "tomatoes", "brinjal"}
fruits := []string{"oranges", "apples"}
food := append(veggies, fruits...)
fmt.Println("food:", food)
# food: [potatoes tomatoes brinjal oranges apples]

# 切片的函数传递
func subtactOne(numbers []int) {
	for i := range numbers {
		numbers[i] -= 2
	}
}
func main() {
	nos := []int{8, 7, 6}
	fmt.Println("slice before function call", nos)
	subtactOne(nos)                               // function modifies the slice 传递切片,类似引用传递,不能直接使用数组传递
	fmt.Println("slice after function call", nos) // modifications are visible outside
}

# 多维切片一样的

# 内存优化
# 切片持有对底层数组的引用。只要切片在内存中,数组就不能被垃圾回收。在内存管理方面,这是需要注意的。让我们假设我们有一个非常大的数组,我们只想处理它的一小部分。然后,我们由这个数组创建一个切片,并开始处理切片。这里需要重点注意的是,在切片引用时数组仍然存在内存中。
# 一种解决方法是使用 copy 函数 func copy(dst,src[]T)int 来生成一个切片的副本。这样我们可以使用新的切片,原始数组可以被垃圾回收。
countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
neededCountries := countries[:len(countries)-2]
countriesCpy := make([]string, len(neededCountries)) // 长度为len(neededCountries)的切片,空的,但是并不是nil
copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy 现在 countries 数组可以被垃圾回收, 因为 neededCountries 不再被引用,已经copy到countriesCpy
return countriesCpy

可变参数函数

# append 函数是如何将任意个参数值加入到切片中的。这样 append 函数可以接受不同数量的参数。
# 可变参数函数的工作原理是把可变参数转换为一个新的切片。
# 我们没有给可变参数 nums ...int 传入任何参数。这也是合法的,在这种情况下 nums 是一个长度和容量为 0 的 nil 切片。
func find(num int, nums ...int) { // 最后一个参数...,接受所有的值,num = 89 ,nums = [89 90 95]
	fmt.Printf("type of nums is %T\n", nums)
	found := false
	for i, v := range nums {
		if v == num {
			fmt.Println(num, "found at index", i, "in", nums)
			found = true
		}
	}
	if !found {
		fmt.Println(num, "not found in ", nums)
	}
	fmt.Printf("\n")
}
func main() {
	find(89, 89, 90, 95)
}

# 给可变参数函数传入切片
# 有一个可以直接将切片传入可变参数函数的语法糖,你可以在在切片后加上 ... 后缀。如果这样做,切片将直接传入函数,不再创建新的切片
func find(num int, nums ...int) {
    fmt.Printf("type of nums is %T\n", nums)
    found := false
    for i, v := range nums {
        if v == num {
            fmt.Println(num, "found at index", i, "in", nums)
            found = true
        }
    }
    if !found {
        fmt.Println(num, "not found in ", nums)
    }
    fmt.Printf("\n")
}
func main() {
    nums := []int{89, 90, 95} // 长度3 容量3 的切片
    find(89, nums...) // ...在后不再转换传入切片,因为可变参数函数接受的...就是一个切片,不加...就相当于:find(89, []int{nums}),编译器会报错
}
# 2种方式
func modify(arr ...int) { // 接受并创建一个切片
	fmt.Println(arr)
}
func main() {
	a := [3]int{89, 90, 91} // 数组
	b := a[:]               // 切片
	modify(b...)            // 传入非切片,不再创建新的切片
	modify2(b)              // 传入一个切片
}
func modify2(arr []int) { // 接受切片
	fmt.Println(arr)
}
# 测试
func change(s ...string) {
	s[0] = "Go"
}
func main() {
	welcome := []string{"hello", "world"}
	change(welcome...)
	fmt.Println(welcome)
}
# 输出 [Go world]
func change(s ...string) {
	s[0] = "Go"
	s = append(s, "playground")
	fmt.Println(s)
}
func main() {
	welcome := []string{"hello", "world"}
	change(welcome...)
	fmt.Println(welcome)
}
# 输出[Go world playground][Go world]

Maps

# map 是在 Go 中将值(value)与键(key)关联的内置类型。通过相应的键可以获取到值。
# 创建,通过向 make 函数传入键和值的类型,可以创建 map。make(map[type of key]type of value) 是创建 map 的语法。
personSalary := make(map[string]int) // 不为nil的空map
var personSalary map[string]int // 为nil的空map,map 的零值是 nil,创建map之后,必须make初始化才能赋值
personSalary = make(map[string]int)
# make之后才能赋值
personSalary["steve"] = 12000
# 
personSalary := map[string]int {
    "steve": 12000,
    "jamie": 15000,
}
personSalary["mike"] = 9000
fmt.Println("personSalary map contents:", personSalary)
fmt.Println(personSalary["mike"]) // 获取

# map 中到底是不是存在这个 key,如果 ok 是 true,表示 key 存在,key 对应的值就是 value ,反之表示 key 不存在。
value, ok := personSalary["mike"]
if ok {
	fmt.Println(value)
}
# 遍历
for key, value := range personSalary {
    fmt.Printf("personSalary[%s] = %d\n", key, value)
}
# 操作元素
# 删除 map 中 key 的语法是 delete(map, key)。这个函数没有返回值。
# map的长度len(map)

# 注意:Map 是引用类型,和 slices 类似,当 map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。
personSalary := map[string]int{
	"steve": 12000,
	"jamie": 15000,
}
personSalary["mike"] = 9000
fmt.Println("Original person salary", personSalary)
newPersonSalary := personSalary
newPersonSalary["mike"] = 18000
fmt.Println("Person salary changed", personSalary)
# 输出
# Original person salary map[steve:12000 jamie:15000 mike:9000]
# Person salary changed map[steve:12000 jamie:15000 mike:18000]

# Map 的相等性
# map 之间不能使用 == 操作符判断,== 只能用来检查 map 是否为 nil,如需对比,请自行便遍历对比

字符串

# 字符串是一个字节切片,打印每一个字符
func printBytes(s string) {
    for i:= 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i]) // %x 格式限定符用于指定 16 进制编码。%c 格式限定符用于打印字符串的字符。
    }
}
func main() {
    name := "Hello World"
    printBytes(name) // 48 65 6c 6c 6f 20 57 6f 72 6c 64
}
# rune
# rune 是 Go 语言的内建类型,它也是 int32 的别称。在 Go 语言中,rune 表示一个代码点。代码点无论占用多少个字节,都可以用一个 rune 来表示。让我们修改一下上面的程序,用 rune 来打印字符。
func printChars(s string) {
	runes := []rune(s)
	for i := 0; i < len(runes); i++ {
		fmt.Printf("%c ", runes[i])
	}
}
func main() {
	name := "Señor" // ñ 占了两个字节,需要使用rune
	printChars(name)
	// 遍历
	for index, rune := range name {
		fmt.Printf("%c starts at byte %d\n", rune, index)
	}
}

# 用字节切片构造字符串
byteSlice := []byte{0x43, 0x61, 0x66, 0xC3, 0xA9}
str := string(byteSlice)
fmt.Println(str) // Café,上面的程序中 byteSlice 包含字符串 Café 用 UTF-8 编码后的 16 进制字节。程序输出结果是 Café

byteSlice := []byte{67, 97, 102, 195, 169}// 把 16 进制换成对应的 10 进制
str := string(byteSlice)
fmt.Println(str)
# 字符串的长度
# utf8 package 包中的 func RuneCountInString(s string) (n int) 方法用来获取字符串的长度。这个方法传入一个字符串参数然后返回字符串中的 rune 的数量。
import (  
    "fmt"
    "unicode/utf8"
)
# 注意:字符串是不可变的,Go 中的字符串是不可变的。一旦一个字符串被创建,那么它将无法被修改。
func mutate(s string)string {  
    s[0] = 'a'//any valid unicode character within single quote is a rune  编译器会报错 cannot assign to s[0]
    return s
}
func main() {  
    h := "hello"
    fmt.Println(mutate(h))
}
# 为了修改字符串,可以把字符串转化为一个 rune 切片。然后这个切片可以进行任何想要的改变,然后再转化为一个字符串。
func mutate(s []rune) string {  // 接收一个 rune 切片参数
    s[0] = 'a' 
    return string(s)
}
func main() {  
    h := "hello"
    fmt.Println(mutate([]rune(h)))
}

指针

# 指针变量的类型为 *T,该指针指向一个 T 类型的变量,指针的零值是 nil。Go 不支持指针运算。
b := 255
var a *int = &b // 声明a是一个指针类型,使用&获取b的内存地址
fmt.Printf("Type of a is %T\n", a) // Type of a is *int
fmt.Println("address of b is", a) // address of b is 0xc4200160f8

# 指针的解引用
b := 255
a := &b
fmt.Println("address of b is", a) // address of b is 0xc4200160f8
fmt.Println("value of b is", *a) // value of b is 255

# 修改值
b := 255
a := &b
fmt.Println("address of b is", a) // address of b is 0xc4200160f8
fmt.Println("value of b is", *a) // value of b is 255
*a++
fmt.Println("new value of b is", b) // new value of b is 256

# 向函数传递指针参数
func change(val *int) {  // 接受指针类型
    *val = 55
}
func main() {  
    a := 58
    fmt.Println("value of a before function call is",a)
    b := &a
    change(b)
    fmt.Println("value of a after function call is", a)
}

# 注意:不要向函数传递数组的指针,而应该使用切片
# 假如我们想要在函数内修改一个数组,并希望调用函数的地方也能得到修改后的数组,一种解决方案是把一个指向数组的指针传递给这个函数。
func modify(arr *[3]int) {  
    (*arr)[0] = 90 // 把 arr 解引用,将 90 赋值给这个数组的第一个元素,简写:arr[0] = 90
}
func main() {  
    a := [3]int{89, 90, 91}
    modify(&a) // 将数组的地址传递给了 modify 函数。 使用切片modify(a[:]),接受切片modify(arr []int)
    fmt.Println(a) // [90 90 91]
}

结构体

# 结构体是用户定义的类型,表示若干个字段(Field)的集合。有时应该把数据整合在一起,而不是让这些数据没有联系。这种情况下可以使用结构体。
# 先声明
type Employee struct {
    firstName string
    lastName  string
    age       int
}
# 赋值
emp1 := Employee{
    firstName: "Sam",
    age:       25,
    lastName:  "Anderson",
}
# 创建匿名结构体,不声明
emp3 := struct {
	firstName, lastName string
	age, salary         int
}{
	firstName: "Andreah",
	lastName:  "Nikola",
	age:       31,
	salary:    5000,
}
fmt.Println("Employee 3", emp3)
# 结构体的零值(Zero Value)使用类型的默认值
var emp4 Employee //zero valued structure
fmt.Println("Employee 4", emp4) // Employee 4 {  0}
# 访问结构体的字段
emp6 := Employee{"Sam", "Anderson", 55}
fmt.Println("First Name:", emp6.firstName)
# 创建零值的 struct,以后再给各个字段赋值。
var emp7 Employee
emp7.firstName = "Jack"
emp7.lastName = "Adams"
fmt.Println("Employee 7:", emp7)

# 结构体的指针
func main() {  
    emp8 := &Employee{"Sam", "Anderson", 55, 6000}
    fmt.Println("First Name:", (*emp8).firstName) // First Name: Sam go可以省略(*emp8)为emp8
    fmt.Println("Age:", emp8.age) // Age: 55
    fmt.Println("Age:", (*emp8).age) // Age: 55
}

# 匿名字段
type Person struct {  
    string
    int
}
func main() {  
    p := Person{"Naveen", 50}
    fmt.Println(p.string)
}

# 嵌套结构体(Nested Structs)一样的,当字段唯一时,可以提升字段
func main() {  
    var p Person
    p.name = "Naveen"
    p.age = 50
    p.Address = Address{
        city:  "Chicago",
        state: "Illinois",
    }
    fmt.Println("Name:", p.name)
    fmt.Println("Age:", p.age)
    fmt.Println("City:", p.city) //city is promoted field 提升字段
    fmt.Println("State:", p.state) //state is promoted field 提升字段
}
# 导出结构体和字段
# 如果结构体名称以大写字母开头,则它是其他包可以访问的导出类型(Exported Type)。同样,如果结构体里的字段首字母大写,它也能被其他包访问到。
var spec computer.Spec // computer包里面的结构体Spec
spec.Maker = "apple"
spec.Price = 50000
fmt.Println("Spec:", spec)

# 结构体相等性(Structs Equality)
# 结构体是值类型。如果它的每一个字段都是可比较的,则该结构体也是可比较的。如果两个结构体变量的对应字段相等,则这两个变量也是相等的。如果结构体包含不可比较的字段,则结构体变量也不可比较。比如map

方法

# 方法其实就是一个函数,在 func 这个关键字和方法名中间加入了一个特殊的 接收器 类型。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的。
# 创建了一个接收器类型为 Type 的方法 methodName
func (t Type) methodName(parameter list) {
}
# 例子
package main

import (
    "fmt"
)

type Employee struct {
    name     string
    salary   int
    currency string
}

/*
  displaySalary() 方法将 Employee 做为接收器类型
*/
func (e Employee) displaySalary() {
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

func main() {
    emp1 := Employee {
        name:     "Sam Adolf",
        salary:   5000,
        currency: "$",
    }
    emp1.displaySalary() // 调用 Employee 类型的 displaySalary() 方法
}
# 使用函数写出相同的程序,为什么我们需要方法
# Go 不是纯粹的面向对象编程语言,而且Go不支持类。因此,基于类型的方法是一种实现和类相似行为的途径。
# 例子
package main

import (
    "fmt"
    "math"
)

type Rectangle struct {
    length int
    width  int
}

type Circle struct {
    radius float64
}

func (r Rectangle) Area() int {
    return r.length * r.width
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func main() {
    r := Rectangle{
        length: 10,
        width:  5,
    }
    fmt.Printf("Area of rectangle %d\n", r.Area())
    c := Circle{
        radius: 12,
    }
    fmt.Printf("Area of circle %f", c.Area())
}

# 指针接收器与值接收器
# 还可以创建使用指针接收器的方法。值接收器和指针接收器之间的区别在于, 在指针接收器的方法内部的改变对于调用者是可见的 ,然而值接收器的情况不是这样的。
package main

import (
	"fmt"
)

type Employee struct {
	name string
	age  int
}

/*
使用值接收器的方法。
*/
func (e Employee) changeName(newName string) {
	e.name = newName
}

/*
使用指针接收器的方法。
*/
func (e *Employee) changeAge(newAge int) {
	e.age = newAge
}

func main() {
	e := Employee{
		name: "Mark Andrew",
		age:  50,
	}
	e.changeAge(50)
	fmt.Println(e) // {Mark Andrew 50} 所做的改变对调用者是不可见的,没有改变值
	(&e).changeAge(100) {Mark Andrew 100} 所做的改变对调用者是不可见的,改变值  如果函数不重名,那么可以直接e.changeAge(100),省略&
	fmt.Println(e)
}
# 那么什么时候使用指针接收器,什么时候使用值接收器
# 一般来说,指针接收器可以使用在:对方法内部的接收器所做的改变应该对调用者可见时。
# 指针接收器也可以被使用在如下场景:当拷贝一个结构体的代价过于昂贵时。考虑下一个结构体有很多的字段。在方法内使用这个结构体做为值接收器需要拷贝整个结构体,这是很昂贵的。在这种情况下使用指针接收器,结构体不会被拷贝,只会传递一个指针到方法内部使用。
# 在其他的所有情况,值接收器都可以被使用。

# 匿名字段的方法
# 属于结构体的匿名字段的方法可以被直接调用,就好像这些方法是属于定义了匿名字段的结构体一样。
# 即在多重结构体里面,使用 p.fullAddress() 来访问 address 结构体的 fullAddress() 方法。明确的调用 p.address.fullAddress() 是没有必要的。

# 在方法中使用值接收器 与 在函数中使用值参数
# 当一个函数有一个值参数,它只能接受一个值参数。
# 当一个方法有一个值接收器,它可以接受值接收器和指针接收器。
# 例子
package main

import (
	"fmt"
)

type rectangle struct {
	length int
	width  int
}

func area(r rectangle) {
	fmt.Printf("Area Function result: %d\n", (r.length * r.width))
}

func (r rectangle) area() {
	fmt.Printf("Area Method result: %d\n", (r.length * r.width))
}

func main() {
	r := rectangle{
		length: 10,
		width:  5,
	}
	area(r) // 调用Function
	r.area()

	p := &r
	//area(p) // 这里p是指针,编译会报错,Function不接受指针

	p.area() // 通过指针调用值接收器,那么就是调用Method,接受指针
}
# Area Function result: 50
# Area Method result: 50
# Area Method result: 50

# 在方法中使用指针接收器 与 在函数中使用指针参数
# 和值参数相类似,函数使用指针参数只接受指针,而使用指针接收器的方法可以使用值接收器和指针接收器。
# 例子
package main

import (
    "fmt"
)

type rectangle struct {
    length int
    width  int
}

func perimeter(r *rectangle) {
    fmt.Println("perimeter function output:", 2*(r.length+r.width))

}

func (r *rectangle) perimeter() {
    fmt.Println("perimeter method output:", 2*(r.length+r.width))
}

func main() {
    r := rectangle{
        length: 10,
        width:  5,
    }
    p := &r
    perimeter(p) // 传递指针,function (r *rectangle)接受指针
    p.perimeter() // 传递指针,method本来就接收指针

    //perimeter(r) // 这里不是指针,编译器报错cannot use r (type rectangle) as type *rectangle in argument to perimeter

    r.perimeter()// 使用值来调用指针接收器,可以使用值接收器和指针接收器
    (&r).perimeter() // 与上面一样
}
# perimeter function output: 30
# perimeter method output: 30
# perimeter method output: 30

# 在非结构体上的方法
# 为了在一个类型上定义一个方法,方法的接收器类型定义和方法的定义应该在同一个包中。到目前为止,我们定义的所有结构体和结构体上的方法都是在同一个 main 包中,因此它们是可以运行的。
# 例子
package main

import "fmt"

type myInt int

func (a myInt) add(b myInt) myInt {
	return a + b
}

func main() {
	// num1 := myInt(5) // 一样的
	var num1 myInt = 5
	num2 := myInt(10)
	sum := num1.add(num2)
	fmt.Println("Sum is", sum)
}

接口(一)
Go 系列教程 —— 18. 接口(一)

# 在面向对象的领域里,接口一般这样定义:接口定义一个对象的行为。接口只指定了对象应该做什么,至于如何实现这个行为(即实现细节),则由对象本身去确定。
type SalaryCalculator interface {
	CalculateSalary() int // 这是一个函数,可以有多个接受类型
}
# 接口实现
func (p Permanent) CalculateSalary() int {
	return p.basicpay + p.pf  // Permanent是basicpay+pf
}
func (c Contract) CalculateSalary() int {
	return c.basicpay // Contract
}
# Permanent和Contract使用的是2个结构体
type Permanent struct {
	empId    int
	basicpay int
	pf       int
}
type Contract struct {
	empId    int
	basicpay int
}
# 赋值给结构体
pemp1 := Permanent{1, 5000, 20} // Permanent是basicpay+pf
cemp1 := Contract{3, 3000} // Contract是basicpay
# 那么计算多个pempN和cempN就会变得容易
employees := []SalaryCalculator{pemp1, cemp1} // 创建一个接口类型 SalaryCalculator 的切片
totalExpense(employees) // 调用计算函数
# 
func totalExpense(s []SalaryCalculator) { // 接收 接口类型
	expense := 0
	for _, v := range s {
		expense = expense + v.CalculateSalary() // 累计数量
	}
	fmt.Printf("Total Expense Per Month $%d", expense)
}

# 接口的内部表示
# 我们可以把接口看作内部的一个元组 (type, value)。 type 是接口底层的具体类型(Concrete Type),而 value 是具体类型的值。
# 例子
package main
import (  
    "fmt"
)
type Test interface {  // Test 接口
    Tester()
}
type MyFloat float64
func (m MyFloat) Tester() {  // MyFloat 类型实现了该接口
    fmt.Println(m)
}
func describe(t Test) {  
    fmt.Printf("Interface type %T value %v\n", t, t)
}

func main() {  
    var t Test // t 为接口类型
    f := MyFloat(89.7) // f 为MyFloat类型
    t = f // 把MyFloat类型赋给Test接口类型
    describe(t) // Interface type main.MyFloat value 89.7
    t.Tester() // 89.7 t的MyFloat类型调用t的Test接口,以及实现Tester()
}

# 空接口
# 没有包含方法的接口称为空接口。空接口表示为 interface{}。由于空接口没有方法,因此所有类型都实现了空接口。
# 这个函数可以传递任何类型。
func describe(i interface{}) {  
    fmt.Printf("Type = %T, value = %v\n", i, i)
}
s := "Hello World"
describe(s)
i := 55
describe(i)
strt := struct { // 匿名结构体
    name string
}{
    name: "Naveen R", // 直接赋值
}
describe(strt)

# 类型断言
# 类型断言用于提取接口的底层值(Underlying Value)。在语法 i.(T) 中,接口 i 的具体类型是 T,该语法用于获得接口的底层值。
package main

import (  
    "fmt"
)

func assert(i interface{}) {  
    v, ok := i.(int) // 检测是不是有int类型
    fmt.Println(v, ok) // 输出v
}
func main() {  
    var s interface{} = 56
    assert(s)
    var i interface{} = "Steven Paul"
    assert(i)
}
# 类型选择(Type Switch)
# 类型选择用于将接口的具体类型与很多 case 语句所指定的类型
func findType(i interface{}) {  
    switch i.(type) {
    case string:
        fmt.Printf("I am a string and my value is %s\n", i.(string))
    case int:
        fmt.Printf("I am an int and my value is %d\n", i.(int))
    default:
        fmt.Printf("Unknown type\n")
    }
}
func main() {  
    findType("Naveen")
    findType(77)
    findType(89.98) // 89.98 的类型是 float64,没有在 case 上匹配成功
}

接口(二)

# 我们在讨论方法的时候就已经提到过,使用值接受者声明的方法,既可以用值来调用,也能用指针调用。不管是一个值,还是一个可以解引用的指针,调用这样的方法都是合法的。
# 接口就是调用方法,也是一样的。
package main

import "fmt"

type Describer interface {
	Describe()
}
type Person struct {
	name string
	age  int
}

func (p Person) Describe() { // 使用值接受者实现
	fmt.Printf("%s is %d years old\n", p.name, p.age)
}

type Address struct {
	state   string
	country string
}

func (a *Address) Describe() { // 使用指针接受者实现
	fmt.Printf("State %s Country %s", a.state, a.country)
}

func main() {
	var d1 Describer        // 接口类型
	p1 := Person{"Sam", 25} // 把结构体赋值
	d1 = p1                 // 通过不同的结构体使用接口的不同方法
	d1.Describe()           // 既可以用值来调用,也能用指针调用
	d2 := &p1               // 通过不同的结构体使用接口的不同方法
	d2.Describe()           // 既可以用值来调用,也能用指针调用

	var p2 Describer
	a := Address{"Washington", "USA"}
	a.Describe() // 这样是可以的
	//p2 = a       // 不可以,对于使用指针接受者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。 cannot use a (type Address) as type Describer in assignment:Address does not implement Describer (Describe method has pointer receiver)
	//但是接口中存储的具体值(Concrete Value)并不能取到地址,因此,对于编译器无法自动获取 a 的地址,于是程序报错。
	//func (a *Address) Describe()去掉*
	p2 = &a
	p3 := a
	p2.Describe()
	p3.Describe()
}
# 输出
# Sam is 25 years old
# Sam is 25 years old
# State Washington Country USA
# State Washington Country USA
# State Washington Country USA

# 实现多个接口
e := Employee{ // 一个结构体
	firstName:   "Naveen",
	lastName:    "Ramanathan",
	basicPay:    5000,
	pf:          200,
	totalLeaves: 30,
	leavesTaken: 5,
}
// SalaryCalculator和LeaveCalculator接口
// DisplaySalary和DisplaySalary2实现接口
var s SalaryCalculator = e
s.DisplaySalary() // 调用就行
var l LeaveCalculator = e
l.DisplaySalary2() // 调用就行

# 接口的嵌套
# 尽管 Go 语言没有提供继承机制,但可以通过嵌套其他的接口,创建一个新接口。
type SalaryCalculator interface {
	DisplaySalary()
}

type LeaveCalculator interface {
	CalculateLeavesLeft() int
}

type EmployeeOperations interface { // 嵌套2个接口
	SalaryCalculator
	LeaveCalculator
}
// 实现
func (e Employee) DisplaySalary() {
	fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}
// 实现
func (e Employee) CalculateLeavesLeft() int {
	return e.totalLeaves - e.leavesTaken
}
// 一样的使用
e := Employee{
	firstName:   "Naveen",
	lastName:    "Ramanathan",
	basicPay:    5000,
	pf:          200,
	totalLeaves: 30,
	leavesTaken: 5,
}
var empOp EmployeeOperations = e
empOp.DisplaySalary() // 使用SalaryCalculator接口的方法

# 接口的零值
# 接口的零值是 nil。对于值为 nil 的接口,其底层值(Underlying Value)和具体类型(Concrete Type)都为 nil。
type Describer interface {  
    Describe()
}
func main() {  
    var d1 Describer
    if d1 == nil {
        fmt.Printf("d1 is nil and has type %T value %v\n", d1, d1)
    }
    d1.Describe() // 对于值为 nil 的接口,由于没有底层值和具体类型,当我们试图调用它的方法时,程序会产生 panic 异常。
}

到了这里时候将开始学习更加特色的golang


并发

# Go 是并发式语言,而不是并行式语言。
# 并发是指立即处理多个任务的能力。我们可以想象一个人正在跑步。假如在他晨跑时,鞋带突然松了。于是他停下来,系一下鞋带,接下来继续跑。这个例子就是典型的并发。这个人能够一下搞定跑步和系鞋带两件事,即立即处理多个任务。
# 并行是指同时处理多个任务。我们同样用这个跑步的例子来帮助理解。假如这个人在慢跑时,还在用他的 iPod 听着音乐。在这里,他是在跑步的同时听音乐,也就是同时处理多个任务。这称之为并行。
# 并行不一定会加快运行速度,因为并行运行的组件之间可能需要相互通信。
# Go 编程语言原生支持并发。Go 使用 Go 协程(Goroutine) 和信道(Channel)来处理并发。

Go 协程

# Go 协程相比于线程的优势
# 相比线程而言,Go 协程的成本极低。堆栈大小只有若干 kb,并且可以根据应用的需求进行增减。而线程必须指定堆栈的大小,其堆栈是固定不变的。
# Go 协程会复用(Multiplex)数量更少的 OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。如果该线程中的某一 Go 协程发生了阻塞(比如说等待用户输入),那么系统会再创建一个 OS 线程,并把其余 Go 协程都移动到这个新的 OS 线程。所有这一切都在运行时进行,作为程序员,我们没有直接面临这些复杂的细节,而是有一个简洁的 API 来处理并发。
# Go 协程使用信道(Channel)来进行通信。信道用于防止多个协程访问共享内存时发生竞态条件(Race Condition)。信道可以看作是 Go 协程之间通信的管道。我们会在下一教程详细讨论信道。

# 启动一个 Go 协程
# 调用函数或者方法时,在前面加上关键字 go,可以让一个新的 Go 协程并发地运行。
package main
import (
	"fmt"
)
func hello() {
	fmt.Println("Hello world goroutine")
}
func main() {
	// go hello() 启动了一个新的 Go 协程。现在 hello() 函数与 main() 函数会并发地执行。
	// 主函数会运行在一个特有的 Go 协程上,它称为 Go 主协程(Main Goroutine)
	go hello()
	fmt.Println("main function")
}
# 输出
# main function
# 即hello()没有运行
# 启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。在调用 Go 协程之后,程序控制会立即返回到代码的下一行,忽略该协程的任何返回值。
# 如果希望运行其他 Go 协程,Go 主协程必须继续运行着。如果 Go 主协程终止,则程序终止,于是其他 Go 协程也不会继续运行。
# 修复这个问题
package main
import (
	"fmt"
	"time"
)
func hello() {
	fmt.Println("Hello world goroutine")
}
func main() {
	go hello()
	// 延迟主协程结束时间
	time.Sleep(1 * time.Second)
	fmt.Println("main function")
}

# 启动多个 Go 协程
# 通过延迟时间进行的交替执行
package main

import (
	"fmt"
	"time"
)

func numbers() {
	for i := 1; i <= 5; i++ {
		time.Sleep(250 * time.Millisecond)
		fmt.Printf("%d ", i)
	}
}
func alphabets() {
	for i := 'a'; i <= 'e'; i++ {
		time.Sleep(400 * time.Millisecond)
		fmt.Printf("%c ", i)
	}
}
func main() {
	go numbers()
	go alphabets()
	time.Sleep(3000 * time.Millisecond)
	fmt.Println("main terminated")
}
# 输出
# 1 a 2 3 b 4 c 5 d e main terminated

信道

# 信道可以想像成 Go 协程之间通信的管道。如同管道中的水会从一端流到另一端,通过使用信道,数据也可以从一端发送,在另一端接收。
# 信道的声明,所有信道都关联了一个类型。信道只能运输这种类型的数据,而运输其他类型的数据都是非法的。
var a chan int // 信道的声明
if a == nil {
   fmt.Println("channel a is nil, going to define it")
   a = make(chan int) // 信道的零值为 nil。信道的零值没有什么用,应该像对 map 和切片所做的那样,用 make 来定义信道。
   fmt.Printf("Type of a is %T", a)
}

# 简短声明通常也是一种定义信道的简洁有效的方法
a := make(chan int)
# 通过信道进行发送和接收
# 发送与接收默认是阻塞的
data := <- a // 读取信道 a , 箭头对于 a 来说是向外指的,因此我们读取了信道 a 的值,并把该值存储到变量 data。
a <- data // 写入信道 a, 箭头指向了 a,因此我们在把数据写入信道 a
# 例子
package main

import (
	"fmt"
)

func hello(done chan bool) {
	fmt.Println("Hello world goroutine")
	done <- true // 写入数据 bool 类型,接下来向 done 写入数据。当完成写入时,Go 主协程会通过信道 done 接收数据,于是它解除阻塞状态,打印出文本 main function。
}
func main() {
	done := make(chan bool) // 类型的管道done
	go hello(done)
	<-done // 通过信道 done 接收数据。这一行代码发生了阻塞,除非有协程向 done 写入数据,否则程序不会跳到下一行代码。于是,这就不需要用以前的 time.Sleep 来阻止 Go 主协程退出了。
	// 主协程发生了阻塞,等待信道 done 发送的数据。
	fmt.Println("main function")
}
# 该程序会计算一个数中每一位的平方和与立方和,然后把平方和与立方和相加并打印出来。
package main

import (
	"fmt"
)

func calcSquares(number int, squareop chan int) {
	sum := 0
	for number != 0 {
		digit := number % 10
		sum += digit * digit
		number /= 10
	}
	squareop <- sum // 写入信道值
}

func calcCubes(number int, cubeop chan int) {
	sum := 0
	for number != 0 {
		digit := number % 10
		sum += digit * digit * digit
		number /= 10
	}
	cubeop <- sum // 写入信道值
}

func main() {
	number := 589
	sqrch := make(chan int)
	cubech := make(chan int)
	go calcSquares(number, sqrch)
	go calcCubes(number, cubech)
	squares, cubes := <-sqrch, <-cubech // 阻塞,等待信道数值
	fmt.Println("Final output", squares+cubes)
}

# 死锁
# 使用信道需要考虑的一个重点是死锁。当 Go 协程给一个信道发送数据时,照理说会有其他 Go 协程来接收数据。如果没有的话,程序就会在运行时触发 panic,形成死锁。

# 单向信道
# 这种信道只能发送或者接收数据。
sendch := make(chan<- int) // 定义了唯送信道,因为箭头指向了 chan
# 需要 信道转换
func sendData(sendch chan<- int) { //  转换为一个唯送信道
	sendch <- 10
}
func main() {
	cha1 := make(chan int) // 双向信道
	go sendData(cha1)
	fmt.Println(<-cha1)
}
// 于是该信道在 sendData 协程里是一个唯送信道,而在 Go 主协程里是一个双向信道。

# 关闭信道和使用 for range 遍历信道
# 如果成功接收信道所发送的数据,那么 ok 等于 true。而如果 ok 等于 false,说明我们试图读取一个关闭的通道。从关闭的信道读取到的值会是该信道类型的零值。例如,当信道是一个 int 类型的信道时,那么从关闭的信道读取的值将会是 0。
v, ok := <- ch
# 例子
func producer(chnl chan int) {
	for i := 0; i < 10; i++ {
		chnl <- i // 增加了很多信道的值
	}
	close(chnl) // 关闭信道,不然会死锁
}
func main() {
	ch := make(chan int)
	go producer(ch)
	for { // 无限循环
		v, ok := <-ch
		if ok == false {
			break // 取值到关闭信道的位置,就跳出去
		}
		fmt.Println("Received ", v, ok) // 遍历打印ok的信道值
	}
}
// 使用range
func main() {  
    ch := make(chan int)
    go producer(ch)
    for v := range ch { // ch是很多信道的值,需要使用close()关闭,不然死锁
        fmt.Println("Received ",v)
    }
}

缓冲信道和工作池(Buffered Channels and Worker Pools)

# 创建一个有缓冲(Buffer)的信道。只在缓冲已满的情况,才会阻塞向缓冲信道(Buffered Channel)发送数据。同样,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据。
# 同样,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据。
# 通过向 make 函数再传递一个表示容量的参数(指定缓冲的大小),可以创建缓冲信道。
ch := make(chan type, capacity) // capacity无缓冲信道的容量默认为 0
# 例子
func main() {
	ch := make(chan string, 2) // 创建容量为 2 的信道
	ch <- "naveen"             // 不会发生阻塞
	ch <- "paul"               // 不会发生阻塞
	// ch <- "three"           // 死锁,容量为2的信道只能接受2个写入,提前fmt.Println(<-ch)读取,释放缓存就能再次写入了
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}
# 例子
func write(ch chan int) {
	for i := 0; i < 5; i++ {
		ch <- i // ch的缓存是2,当写完2个的时候,就会发生阻塞,直到 ch 内的值被读取
		fmt.Println("successfully wrote", i, "to ch")
	}
	close(ch) // 必须关掉信道
}
func main() {
	ch := make(chan int, 2)
	go write(ch)
	time.Sleep(2 * time.Second)
	for v := range ch { // ch 内的值被读取,缓存被释放,ch <- i就又可以进行写入了
		fmt.Println("read value", v, "from ch")
		time.Sleep(2 * time.Second)
	}
}

# 长度 vs 容量
# 缓冲信道的容量是指信道可以存储的值的数量。我们在使用 make 函数创建缓冲信道的时候会指定容量大小。
# 缓冲信道的长度是指信道中当前排队的元素个数。
ch := make(chan string, 3)
ch <- "naveen"
ch <- "paul"
fmt.Println("capacity is", cap(ch)) // capacity is 3
fmt.Println("length is", len(ch))   // length is 2
fmt.Println("read value", <-ch)     //读取释放缓存
fmt.Println("length is", len(ch))   // length is 1

# WaitGroup
# WaitGroup 用于等待一批 Go 协程执行结束。程序控制会一直阻塞,直到这些协程全部执行完毕。
# 例子
package main

import (
	"fmt"
	"sync"
	"time"
)

func process(i int, wg *sync.WaitGroup) { // 接受 wg 的地址
	fmt.Println("started Goroutine ", i)
	time.Sleep(2 * time.Second)
	fmt.Printf("Goroutine %d ended\n", i)
	(wg).Done() // 减少计数器,
}

func main() {
	no := 3
	var wg sync.WaitGroup // 创建了 WaitGroup 类型的变量,其初始值为零值
	for i := 0; i < no; i++ {
		wg.Add(1)          // WaitGroup 使用计数器来工作。当我们调用 WaitGroup 的 Add 并传递一个 int 时,WaitGroup 的计数器会加上 Add 的传参。
		go process(i, &wg) // 这里的计数器会变成3,同时创建3个go协程,传递 wg 的地址是很重要的。如果没有传递 wg 的地址,那么每个 Go 协程将会得到一个 WaitGroup 值的拷贝,因而当它们执行结束时,main 函数并不会知道。
	}
	wg.Wait() // Wait() 方法会阻塞调用它的 Go 协程,这里会等待3个go协程的wg.Done()减去直到计数器变为0
	fmt.Println("All go routines finished executing")
}

# 工作池的实现
# 缓冲信道的重要应用之一就是实现工作池。
# 一般而言,工作池就是一组等待任务分配的线程。一旦完成了所分配的任务,这些线程可继续等待任务的分配。
# 我们会使用缓冲信道来实现工作池。

# 创建一个 Go 协程池,监听一个等待作业分配的输入型缓冲信道。
# 将作业添加到该输入型缓冲信道中。
# 作业完成后,再将结果写入一个输出型缓冲信道。
# 从输出型缓冲信道读取并打印结果。
# https://studygolang.com/articles/12512
展开阅读全文

没有更多推荐了,返回首页