Golang基础语法
1 Go 程序开发的注意事项
- Go 源文件以 “go” 为扩展名。
- Go 应用程序的执行入口是 main()函数。
- Go 方法由一条条语句构成,每个语句后不需要分号(Go 语言会在每行后自动加分号),这也体现出 Golang 的简洁性。
- Go 编译器是一行行进行编译的,因此我们一行就写一条语句,不能把多条语句写在同一个,否则报错
- go 语言定义的变量或者 import 的包如果没有使用到,代码不能编译通过。
- Go语言不允许大括号换行
2 Golang 变量
2.1 Golang 变量使用的三种方式
(1) 第一种:指定变量类型,声明后若不赋值,使用默认值
package main
import "fmt"
func main() {
var i int
fmt.Println("i = ", i) //i = 0
}
(2) 第二种:根据值自行判定变量类型(类型推导)
package main
import "fmt"
func main() {
var num = 10.11
fmt.Println("num =", num)//num = 10.11
}
(3) 第三种:省略 var, 注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误
package main
import "fmt"
func main() {
name := "tom"
fmt.Println("name= ", name)//name= tom
}
(4) 多变量声明
package main
import "fmt"
//定义全局变量
var (
n4 = 100
name2 = "mary"
)
func main() {
//声明局部变量
n1, name, n3 := 100, "tom", 888
fmt.Println("n1 = ", n1, "name = ", name, "n3 = ", n3)
fmt.Println("n4 = ", n4, "name2 = ", name2)
}
(5) 该区域的数据值可以在同一类型范围内不断变化(重点)
package main
import "fmt"
func main() {
var i int = 10
i = 30
i = 50
fmt.Println(i)
i = 1.2//报错,因为不能改变数据类型
}
2.2 Golang 数据类型的基本介绍
2.2.1 整数类型
2.2.1.1 整型的使用细节
(1)Golang 的整型默认声明为 int 型
(2)如何在程序查看某个变量的字节大小和数据类型 (使用较多)
package main
import (
"fmt"
"unsafe"
)
func main() {
var i = 100
fmt.Printf("i的类型为 %T i的占用字节数为 %d", i, unsafe.Sizeof(i))//i的类型为 int i的占用字节数为 8
}
2.2.2 小数类型分类
2.2.3 字符类型
Golang 中没有专门的字符类型,如果要存储单个字符(字母),一般使用 byte 来保存
package main
import "fmt"
func main() {
var c1 byte = 'a'
var c2 byte = 'b'
fmt.Println("c1 = ", c1)//97
fmt.Println("c2 = ", c2)//98
fmt.Printf("c1=%c c2=%c", c1, c2)//a b
var c3 int = '北'
fmt.Printf("c3=%c c3对应的码值=%d", c3, c3)//北 21271
}
- 如果我们保存的字符在 ASCII 表的,比如[0-1, a-z,A-Z…]直接可以保存到 byte
- 如果我们保存的字符对应码值大于 255,这时我们可以考虑使用 int 类型保存
- 如果我们需要安装字符的方式输出,这时我们需要格式化输出,即 fmt.Printf(“%c”, c1)
- 字符型 存储到 计算机中,需要将字符对应的码值(整数)找出来
存储:字符—>对应码值---->二进制–>存储
读取:二进制----> 码值 ----> 字符 --> 读取 - Go 语言的编码都统一成了 utf-8。非常的方便,很统一,再也没有编码乱码的困扰了
2.2.4 string 类型
字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本
2.2.4.1 string 使用注意事项和细节
- Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本,这样 Golang 统一使用 UTF-8 编码,中文乱码问题不会再困扰程序员。
- 反引号,以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击、输出源代码等效果。
3)当一行字符串太长时,需要使用到多行字符串,可以如下处理
2.2.1 基本数据类型的相互转换
Golang 和 java / c 不同,Go 在不同类型的变量之间赋值时需要显式转换。也就是说 Golang 中数据类型不能自动转换。
package main
import "fmt"
func main() {
var i int32 = 100
var n1 float32 = float32(i)
var n2 int8 = int8(i)
var n3 = int64(i)
fmt.Printf("i=%v n1=%v n2=%v n3=%v n3=%T", i, n1, n2, n3, n3)
}
2.2.2 基本数据类型和 string 的转换
package main
import "fmt"
func main() {
var num1 int = 99
var num2 float64 = 23.22
var b bool = true
var mychar byte = 'h'
var str string
//使用第一种方式转为string
str = fmt.Sprintf("%d", num1)
fmt.Printf("str的type=%T str的值=%q", str, str)
str = fmt.Sprintf("%f", num2)
fmt.Printf("str的type=%T str的值=%q", str, str)
str = fmt.Sprintf("%t", b)
fmt.Printf("str的type=%T str的值=%q", str, str)
str = fmt.Sprintf("%c", mychar)
fmt.Printf("str的type=%T str的值=%q", str, str)
}
2.2.3 string 类型转基本数据类型
package main
import (
"fmt"
"strconv"
)
func main() {
//转换类型要匹配
var str string = "true"
var b bool
b, _ = strconv.ParseBool(str)
fmt.Printf("b的type=%T b=%v", b, b)
fmt.Printf("str的type=%T b=%v", str, str)
var str2 = "123"
var n1 int64
var n2 int
n1, _ = strconv.ParseInt(str2, 10, 64)
//string转换为int返回值类型为int64,如果转换为其他需要显示转化内
n2 = int(n1)
fmt.Printf("n1的type=%T n1=%v", n1, n1)
fmt.Printf("n2的type=%T n2=%v", n2, n2)
var str3 string = "123.22"
var f1 float64
f1, _ = strconv.ParseFloat(str3, 64)
fmt.Printf("f1的type=%T f1=%v", f1, f1)
}
注意:string转换为整形的时候,返回的类型为int64或float64,因此如果需要转换为int或float32需要显示转换
2.3 指针
- 基本数据类型,变量存的就是值,也叫值类型,包括:int 系列, float 系列, bool, string 、数组和结构体 struct
- 指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值
比如:var ptr *int = &num
- 获取指针类型所指向的值,使用:*,比如:var ptr *int, 使用
*ptr
获取 ptr 指向的值
2.3.1 值类型和引用类型
- 值类型:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
- 引用类型:指针、slice 切片、map、管道 chan、interface 等都是引用类型
- 值类型:变量直接存储值,内存通常在栈中分配
- 引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由 GC 来回收
2.4 标识符的命名规范
- 包名:保持 package 的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,不要和标准库不要冲突 fmt
- 如果变量名、函数名、常量名首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用 ( 注:可以简单的理解成,首字母大写是公开的,首字母小写是私有的) ,在 golang 没有public , private 等关键字。
2.5 运算符
- Golang 的自增自减只能当做一个独立语言使用时,不能这样使用
- Golang 的++ 和 – 只能写在变量的后面,不能写在变量的前面,即:只有
a++ a--
没有++a--a
2.5.1 for 循环的使用注意事项和细节讨论
- Golang 提供 for-range 的方式,可以方便遍历字符串和数组
func main() {
str := "asc!ddd"
for index, val := range str {
fmt.Printf("index=%d,val=%c \n", index, val)
}
}
- for-range 遍历方式而言,是按照字符方式遍历。因此如果有字符串有中文,也是 ok(传统的是按照字节的方式,因此中文会报错)
package main
import "fmt"
func main() {
str := "asc!ddd北京"
for index, val := range str {
fmt.Printf("index=%d,val=%c \n", index, val)
}
}
3 函数、包和错误处理
3.1 Golang中函数的基本语法
package main
import "fmt"
func cal(n1 float64, n2 float64, operator byte) float64 {
var res float64
switch operator {
case '+':
res = n1 + n2
case '-':
res = n1 - n2
case '*':
res = n1 * n2
case '/':
res = n1 / n2
default:
fmt.Println("操作错误")
}
return res
}
func main() {
var n1 float64 = 1.2
var n2 float64 = 0.6
var operator byte = '+'
res := cal(n1, n2, operator)
fmt.Printf("res=%v", res)
}
3.1.1 Golang中函数的注意事项
- Go函数支持多个返回值
- 希望忽略某个返回值,则使用 _ 符号表示占位忽略
- 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。
- 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。
- Go 函数不支持函数重载
- 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
- 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用
- 为了简化数据类型定义,Go 支持自定义数据类型
基本语法:type 自定义数据类型名 数据类型 // 理解: 相当于一个别名
- 支持对函数返回值命名
3.1.2 Golang中匿名函数
匿名函数使用方式 1
在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次
package main
import "fmt"
func main() {
res1 := func(n1 int, n2 int) int {
return n1 + n2
}(12, 20)
fmt.Println("res1=", res1)
}
匿名函数使用方式 2
将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
package main
import "fmt"
func main() {
a := func(n1 int, n2 int) int {
return n1 - n2
}
res2 := a(10, 30)
fmt.Println("res2=\n", res2)
res3 := a(21, 22)
fmt.Println("res3=", res3)
}
全局匿名函数
如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。
package main
import "fmt"
var (
Fun1 = func(n1 int, n2 int) int {
return n1 * n2
}
)
func main() {
res4 := Fun1(4, 2)
fmt.Println("res3=", res4)
}
3.2 Golang包的相关说明
- 在给一个文件打包时,该包对应一个文件夹,文件的包名通常和文件所在的文件夹名一致,一般为小写字母。
- 当一个文件要使用其它包函数或变量时,需要先引入对应的包
引入方式 1:import “包名”
引入方式 2:
import (
“包名”
“包名”
) - 在 import 包时,路径从 $GOPATH 的 src 下开始,不用带 src , 编译器会自动从 src 下开始引入
- 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言的 public ,这样才能跨包访问
- 在访问其它包函数,变量时,其语法是 包名.函数名
- 如果包名较长,Go 支持给包取别名, 注意细节:取别名后,原来的包名就不能使用了
- 在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义
- 如果你要编译成一个可执行程序文件,就需要将这个包声明为 main , 即 package main .这个就是一个语法规范,如果你是写一个库 ,包名可以自定义
3.3 Golang的闭包
基本介绍:闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)
package main
import "fmt"
func AddUpper() func(int) int {
var n int = 10
return func(x int) int {
n = n + x
return n
}
}
func main() {
f := AddUpper()
fmt.Println(f(1))
fmt.Println(f(2))
fmt.Println(f(3))
}
返回的是一个匿名函数, 但是这个匿名函数引用到函数外的 n ,因此这个匿名函数就和 n 形成一个整体,构成闭包。
3.4 函数的 defer
在函数执行完毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)。
package main
import "fmt"
func AddUpper() func(int) int {
var n int = 10
return func(x int) int {
n = n + x
return n
}
}
func sum(n1 int, n2 int) int {
defer fmt.Println("ok1")
defer fmt.Println("ok2")
res := n1 + n2
fmt.Println("res1=", res)
return res
}
func main() {
res := sum(10, 20)
fmt.Println("res=", res)
}
3.4.1 函数的 defer注意事项
- 当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个栈中,然后继续执行函数下一个语句。
- 当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制),
- 在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈。
3.5 字符串常用的系统函数
package main
import (
"fmt"
"strconv"
"strings"
)
func AddUpper() func(int) int {
var n int = 10
return func(x int) int {
n = n + x
return n
}
}
func sum(n1 int, n2 int) int {
defer fmt.Println("ok1")
defer fmt.Println("ok2")
res := n1 + n2
fmt.Println("res1=", res)
return res
}
func main() {
str := "hello 北京"
//统计字符串的长度(字节)
fmt.Println("str的长度=", len(str), "\n")
//字符串转成整数int
n, _ := strconv.Atoi("1222")
fmt.Printf("n的type=%T n的值=%d\n", n, n)
//整数转成字符串
str1 := strconv.Itoa(12345)
fmt.Printf("str1的type=%T str1的值=%v\n", str1, str1)
//字符串转[]byte
var bytes = []byte("hellogo")
fmt.Printf("bytes的type=%T bytes=%v\n", bytes, bytes)
//[]byte 转 字符串
str3 := string([]byte{97, 98, 99})
fmt.Printf("str3的type=%T str3=%v\n", str3, str3)
//查找字串是否在指定的字符串中
b := strings.Contains("seafood", "foo")
fmt.Printf("b=%v\n", b) //true
//统计一个字符串有几个指定的子串
count := strings.Count("cehessas", "e")
fmt.Printf("count=%v\n", count)
//不区分大小写的字符串比较(==是区分字母大小写的)
fold := strings.EqualFold("abc", "ABC")
fmt.Printf("fold=%v\n", fold) //ture
//返回子串在字符串第一次出现的 index 值,如果没有返回-1
index := strings.Index("asdasda", "as")
fmt.Printf("index=%v\n", index)
//返回子串在字符串最后一次出现的 index,如没有返回-1
lastIndex := strings.LastIndex("golang go", "go")
fmt.Printf("lastindex=%v\n", lastIndex)
//将指定的子串替换成 另外一个子串
replace := strings.Replace("go go go 你好", "go", "o", -1)
fmt.Printf("replace=%v\n", replace)
//按 照 指 定 的 某 个 字 符 , 为 分 割 标 识 ,
//将 一 个 字 符 串 拆 分 成 字 符 串 数 组
strArr := strings.Split("hello,world,go", ",")
fmt.Printf("strArr的type=%T str=%v\n", strArr, strArr)
//判断字符串是否以指定的字符串开头
prefix := strings.HasPrefix("ftp://11111", "ftp")
fmt.Printf("prefix=%v\n", prefix)
}
3.5 时间函数–日期格式化
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Printf(now.Format("2006-01-02 15:04:05"))
fmt.Println()
fmt.Printf(now.Format("2006.01.02"))
fmt.Println()
fmt.Printf(now.Format("15:04:05"))
}
“2006/01/02 15:04:05” 这个字符串的各个数字是固定的,必须是这样写。
时间常量
const (
Nanosecond Duration = 1 //纳秒
Microsecond = 1000 * Nanosecond //微秒
Millisecond = 1000 * Microsecond //毫秒
Second = 1000 * Millisecond //秒
Minute = 60 * Second //分钟
Hour = 60 * Minute //小时
)
常量的作用:在程序中可用于获取指定时间单位的时间,比如想得到 100 毫秒,100 * time. Millisecond
4 错误处理
- Go 语言追求简洁优雅,所以,Go 语言不支持传统的 try…catch…finally 这种处理。
- Go 中引入的处理方式为:defer, panic, recover
- 这几个异常的使用场景可以这么简单描述:Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理
package main
import (
"fmt"
"time"
)
func test() {
defer func() {
err := recover()
if err != nil {
fmt.Println("err=", err)
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println("res=", res)
}
func main() {
test()
for {
fmt.Println("main")
time.Sleep(time.Second)
}
}
4.1 自定义错误
Go 程序中,也支持自定义错误, 使用 errors.New 和 panic 内置函数。
- errors.New(“错误说明”) , 会返回一个 error 类型的值,表示一个错误
- panic 内置函数 ,接收一个 interface{}类型的值(也就是任何值了)作为参数。可以接收 error 类型的变量,输出错误信息,并退出程序
package main
import (
"errors"
"fmt"
)
func readConf(name string) (err error) {
if name == "hhhh" {
return nil
} else {
return errors.New("读取失败")
}
}
func test() {
err := readConf("hhss")
if err != nil {
panic(err)
}
fmt.Println("test继续执行")
}
func main() {
test()
for {
fmt.Println("main")
}
}
5 数组与切片
5.1 数组
数组可以存放多个同一类型数据。数组也是一种数据类型,在 Go 中,数组是值类型。
- 数组的定义
var 数组名 [数组大小]数据类型
var a [5]int
赋初值 a[0] = 1 a[1] = 30
- 数组初始化的方式
package main
import (
"fmt"
)
func main() {
var numArr01 [3]int = [3]int{1, 2, 3}
fmt.Println("01=", numArr01)
var numArr02 = [3]int{5, 6, 7}
fmt.Println("02=", numArr02)
var numArr03 = [...]int{8, 9, 10}
fmt.Println("03=", numArr03)
var numArr04 = [...]int{1: 11, 0: 6, 2: 7}
fmt.Println("04=", numArr04)
numArr05 := [...]string{1: "11", 0: "6", 2: "7"}
fmt.Println("05=", numArr05)
}
- 数组的遍历–for-range
package main
import "fmt"
func main() {
heros := [...]string{"松江", "吴用", "李白"}
for i, v := range heros {
fmt.Printf("i=%v v=%v", i, v)
}
}
5.2 切片
- 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
- 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice)都一样。
- 切片的长度是可以变化的,因此切片是一个可以动态变化数组。
- 切片定义的基本语法:
var 切片名 []类型
比如:var a [] int
5.2.1 切片在内存中形式
slice 从底层来说,其实就是一个数据结构(struct 结构体)
5.2.1 切片的使用
- 定义一个切片,然后让切片去引用一个已经创建好的数组
package main
import "fmt"
func main() {
intArr := [...]int{1, 2, 3, 4, 5, 6, 7}
slice := intArr[1:3]
fmt.Printf("s=%v", slice)
}
- 通过 make 来创建切片
基本语法:var 切片名 []type = make([]type, len, [cap])
package main
import "fmt"
func main() {
var slice []float64 = make([]float64, 5, 10)
slice[1] = 10
fmt.Printf("s=%v", slice)
}
通过 make 方式创建的切片对应的数组是由 make 底层维护,对外不可见,即只能通过 slice 去访问各个元素.
3. 方式 3 定义一个切片,直接就指定具体数组,使用原理类似 make 的方式
package main
import "fmt"
func main() {
var slice []string = []string{"s1", "s2"}
fmt.Println("s=", slice)
}
5.2.2 切片的使用的注意事项和细节讨论
- 用 append 内置函数,可以对切片进行动态追加
- 切片的拷贝操作
5.2.3 string 和 slice
string 是不可变的,如果想要改变类型,需要转换为其他类型转换
package main
import "fmt"
func main() {
str := "hhhh 你好"
arr1 := []rune(str)
arr1[0] = '你'
str = string(arr1)
fmt.Println("str= ", str)//str= 你hhh 你好
}
5.3 Map
5.3.1 map的基本语法
var map 变量名 map[keytype]valuetype
var a map[string]string
var a map[string]int
var a map[int]string
var a map[string]map[string]string
注意:声明是不会分配内存的,初始化需要 make ,分配内存后才能赋值和使用。
5.3.2 map 的使用
方式 1
方式 2
方式 3
5.3.3 map 的增删改查操作
-
map[“key”] = value //如果 key 还没有,就是增加,如果 key 存在就是修改。
-
map 删除
delete(map,“key”) ,delete 是一个内置函数,如果 key 存在,就删除该 key-value,如果 key 不存在,不操作,但是也不会报错
注意:如果我们要删除 map 的所有 key ,没有一个专门的方法一次删除,可以遍历一下 key, 逐个删除或者 map = make(…),make 一个新的,让原来的成为垃圾,被 gc 回收
-
map 查找
说明:如果 heroes 这个 map 中存在 “no1” , 那么 findRes 就会返回 true,否则返回 false -
map 遍历
map 的遍历使用 for-range 的结构遍历
-
map 切片
切片的数据类型如果是 map,则我们称为 slice of map,map 切片,这样使用则 map 个数就可以动态变化了。
-
map 排序
(1) golang 中没有一个专门的方法针对 map 的 key 进行排序
(2) golang 中的 map 默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历,得到的输出可能不一样
(3) golang 中 map 的排序,是先将 key 进行排序,然后根据 key 值遍历输出即可
5.3.4 map 使用细节
- map 是引用类型,遵守引用类型传递的机制,在一个函数接收 map,修改后,会直接修改原来的 map
- map 的容量达到后,再想 map 增加元素,会自动扩容,并不会发生 panic,也就是说 map 能动态的增长
6 面向对象编程
- Golang 没有类(class),Go 语言的**结构体(struct)**和其它编程语言的类(class)有同等的地位, Golang 是基于 struct 来实现 OOP 特性的。
- Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等
- Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现
6.1 结构体
- 基本语法
定义结构体
type 结构体名称 struct {
field1 type
field2 type
}
创建结构体变量
方式 1-直接声明
package main
import "fmt"
type Persion struct {
Name string
Age int
}
func main() {
var persion Persion
persion.Name = "ss"
persion.Age = 12
fmt.Println("p=", persion)
}
方式 2-{}
package main
import "fmt"
type Persion struct {
Name string
Age int
}
func main() {
p2 := Persion{"mary", 20}
fmt.Println("p2=", p2)
}
方式 3-&
package main
import "fmt"
type Persion struct {
Name string
Age int
}
func main() {
var p3 *Persion = new(Persion)
(*p3).Name = "smish"
p3.Age = 11
fmt.Println("p3=", *p3)
}
方式 4-{}
package main
import "fmt"
type Persion struct {
Name string
Age int
}
func main() {
var p4 *Persion = &Persion{}
(*p4).Name = "kobe"
p4.Age = 99
fmt.Println("p4=", *p4)
}
6.1.1 结构体使用注意事项和细节
- 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
- 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
- struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
6.2 方法
Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct。
6.2.1 方法的声明和调用
type A struct {
Num int
}
func (a A) test() {
fmt.Println(a.Num)
}
- func (a A) test() {} 表示 A 结构体有一方法,方法名为 test
- (a A) 体现 test 方法是和 A 类型绑定的
package main
import "fmt"
type Persion struct {
Name string
}
func (p Persion) test() {
fmt.Println("test name=", p.Name)
}
func main() {
var p Persion
p.Name = "tom"
p.test()
}
- test 方法和 Person 类型绑定
- test 方法只能通过 Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
6.2.2 方法的注意事项
- 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
- 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
- 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出,类似tostring方法
6.2.3 方法和函数区别
- 调用方式不一样
函数的调用方式: 函数名(实参列表)
方法的调用方式: 变量.方法名(实参列表) - 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
- 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
总结:
- 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
- 如果是和值类型,比如 (p Person) , 则是值拷贝, 如果和指针类型,比如是 (p *Person) 则是地址拷贝。
6.3 封装
- 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
- 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
- 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值
func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {
//加入数据验证的业务逻辑
var.字段 = 参数
}
- 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
func (var 结构体类型名) GetXxx() {
return var.age;
}
package model
import "fmt"
type person struct {
Name string
age int
sal float64
}
//编写一个工厂模式,返回结构体对象
func NewPerson(name string) *person {
return &person{
Name: name,
}
}
//为了访问age和sal,设置set和get方法
func (p *person) SetAge(age int) {
if age > 0 && age < 150 {
p.age = age
} else {
fmt.Println("年龄范围不对")
}
}
func (p *person) GetAge() int {
return p.age
}
func (p *person) Setsal(sal float64) {
p.sal = sal
}
func (p *person) Getsal() float64 {
return p.sal
}
package main
import (
"fmt"
"test/model"
)
func main() {
p := model.NewPerson("mary")
p.SetAge(18)
p.Setsal(100000)
fmt.Println(*p)
fmt.Println(p.Name, "age=", p.GetAge(), "sal=", p.Getsal())
}
6.4 继承
在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性
6.4.1 基本语法
type Goods struct {
Name string
Price int
}
type Book struct {
Goods //这里就是嵌套匿名结构体 Goods
Writer string
}
package main
import "fmt"
//定义一个学生的共有属性
type Student struct {
Name string
Age int
Score int
}
//将公有方法也绑定到student
func (stu *Student) ShowInfo() {
fmt.Printf("学生名=%v 年龄=%v 成绩=%v\n",
stu.Name, stu.Age, stu.Score)
}
func (stu *Student) SetScore(score int) {
stu.Score = score
}
//小学生
type Pupil struct {
Student
}
//这是 Pupil 结构体特有的方法,保留
func (p *Pupil) testing() {
fmt.Println("小学生正在考试中.....")
}
//大学生
type Graduate struct {
Student
}
func (p *Graduate) testing() {
fmt.Println("大学生正在考试中.....")
}
func main() {
pupil := &Pupil{}
pupil.Student.Name = "tom~"
pupil.Student.Age = 8
pupil.testing()
pupil.Student.SetScore(70)
pupil.Student.ShowInfo()
graduate := &Graduate{}
graduate.Student.Name = "mary~"
graduate.Student.Age = 28
graduate.testing()
graduate.Student.SetScore(90)
graduate.Student.ShowInfo()
}
6.4.2 继承的使用细节
- 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用
- 匿名结构体字段访问可以简化
- 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错
- 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
- 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
6.5 接口
6.5.1 基本语法
type 接口名 interface{
method(参数列表) 返回值列表
}
- interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量。到某个自定义类型(比如结构体 )要使用的时候,在根据具体情况把这些方法写出来(实现)。
- 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。
- Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字
- interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil
- 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口。
6.6 类型断言
由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
7 文件操作
7.1 打开文件和关闭文件
7.2 读文件操作
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
//打开文件
file, err := os.Open("d:/test.txt")
if err != nil {
fmt.Println("open file err=", err)
}
//当函数退出时,要及时的关闭 file
defer file.Close() //要及时关闭 file 句柄,否则会有内存泄漏. // 创建一个 *Reader ,是带缓冲的
/*
const (
defaultBufSize = 4096 //默认的缓冲区为 4096
)
*/
reader := bufio.NewReader(file)
//循环的读取文件的内容
for {
str, err := reader.ReadString('\n') // 读到一个换行就结束
if err == io.EOF { // io.EOF 表示文件的末尾
break
}
//输出内容
fmt.Print(str)
}
fmt.Println("文件读取结束...")
}
7.3 写文件操作
- 创建一个新文件,写入内容 5 句 “hello, Gardon”
- 打开一个存在的文件中,将原来的内容覆盖成新的内容 10 句 “你好!”
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
//打开一个存在的文件中,将原来的内容覆盖成新的内容 10 句 "你好,尚硅谷!"
//创建一个新文件,写入内容 5 句 "hello, Gardon"
//1 .打开文件已经存在文件, d:/abc.txt
filePath := "d:/abc.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY | os.O_TRUNC, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
//及时关闭 file 句柄
defer file.Close()
//准备写入 5 句 "你好,尚硅谷!"
str := "你好!\r\n" // \r\n 表示换行
//写入时,使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString(str)
}
//因为 writer 是带缓存,因此在调用 WriterString 方法时,其实
//内容是先写入到缓存的,所以需要调用 Flush 方法,将缓冲的数据
//真正写入到文件中, 否则文件中会没有数据!!!
writer.Flush()
}
- 打开一个存在的文件,在原来的内容追加内容 ‘ABC! ENGLISH!’
8 goroutine 和 channel
8.1 协程-goroutine
- 主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
- 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
- Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。
- 如果主线程退出了,则携程即使还没有结束,也会退出
8.2 channel(管道)
goroutine之间通信-解决资源竞争问题
- channle 本质就是一个数据结构-队列
- 数据是先进先出【FIFO : first in first out】
- 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
- channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
8.2.1 定义/声明 channel
var 变量名 chan 数据类型
var intChan chan int (intChan 用于存放 int 数据)
8.2.2 管道的注意事项
- channel 是引用类型
- channel 必须初始化才能写入数据, 即 make 后才能使用
package main
import (
"fmt"
)
func main() {
//演示一下管道的使用
//1. 创建一个可以存放 3 个 int 类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2. 看看 intChan 是什么
fmt.Printf("intChan 的值=%v intChan 本身的地址=%p\n", intChan, &intChan)
//3. 向管道写入数据
intChan <- 10
num := 211
intChan <- num
intChan <- 50
// intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量
//4. 看看管道的长度和 cap(容量)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3
//5. 从管道中读取数据
var num2 int
num2 = <-intChan
fmt.Println("num2=", num2)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 2, 3
//6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock
num3 := <-intChan
num4 := <-intChan
num5 := <-intChan
fmt.Println("num3=", num3, "num4", num4, "num5=", num5)
}
- channel 中只能存放指定的数据类型
- channle 的数据放满后,就不能再放入了
- 如果从 channel 取出数据后,可以继续放入
- 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock
8.2.3 channel 的遍历和关闭
8.2.3.1 关闭
使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据
8.2.3.2 channel 的遍历
channel 支持 for–range 的方式进行遍历,请注意两个细节
- 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
- 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
8.2.4 注意事项
- channel 可以声明为只读,或者只写性质
- goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题
package main
import (
"fmt"
"time"
)
//函数
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}
//函数
func test() {
//这里我们可以使用 defer + recover
defer func() {
//捕获 test 抛出的 panic
if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()
//定义了一个 map
var myMap map[int]string
myMap[0] = "golang" //error
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}