1、go语言概述
go核心编程方向:
- 区块链研发工程师
- go服务器端/游戏软件工程师
- go分布式/云计算软件工程师
go的优势:
- 数据处理
- 高并发
google为什么要创造go语言:
- 硬件技术更新频繁,性能提高很快,现有语言不能合理利用多核多CPU的优势
- 现有语言计算能力不够,处理大并发不够好
- 想兼顾运行速度和开发速度
发展简史:
- 2015年,go1.5版本发布,移除了最后残余的c代码
- 2017年,先后发布了go1.8和1.9
- 2018年,发布了go1.10版本
go的特点:
- go=c+python
- 从c语言继承了很多理念,如指针
- 自动垃圾回收
- 天然并发
- 从语言层面支持并发
- goroutine,轻量级线程,可实现大并发处理,高效利用多核
- 基于CPS并发模型实现
- 通过管道实现不同的goroute之间的相互通信
- 函数可以返回多个值
- 新的语法:延时执行defer、切片slice等
2、基础语法
1、变量使用方式
- 指定变量类型,声明后若不赋值,使用默认值
- 根据值自动推导类型
- 连var也一起省略,但要使用:=符,等价于声明+赋值
var i int
fmt.Println("i=",i)
var j = "hello"
fmt.Println("j=",j)
k := 10.5
fmt.Println("k=",k)
注意:main函数要放在main包下面才能运行,如下图所示。
2、一次性声明多个变量
var i,j,k int
fmt.Println("i=",i,"j=",j,"k=",k)
var o,p,q = 10,"hello",10.5
fmt.Println("o=",o,"p=",p,"q=",q)
r,s,t := 10,"hello",10.5
fmt.Println("r=",r,"s=",s,"t=",t)
3、声明全局变量
package main
var n1 = 100
var n2 = "okk"
var (
n3 = 36.9
n4 = "yes"
)
func main() {
}
4、常量
const a int = 10
常量必须初始化
常量只能修饰bool、数值类型(int、float系列)、string类型
5、go的数据类型
基本数据类型
- 数值型
- 整数类型(int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64)
- 浮点类型(float32,float64)
- 字符型(没有专门的字符型,使用byte来保存单个字母字符)
- 布尔型(bool)
- 字符串
派生/复杂数据类型
- 指针
- 数组
- 结构体
- 管道
- 函数
- 切片(动态数组)
- 接口
- map
6、值类型和引用类型
值类型:基本数据类型、数组、结构体
引用类型:指针、切片、map、管道、interface
7、运算符
golang的自增和自减只能当做独立语句使用,不能b:=a++
golang没有++i
3、go字符与字符串
1、go字符的不同之处
对于其它语言来说,字符串是由一串字符组成的,而go的字符串是由一串字节组成的。
var b1 byte = 'a'
fmt.Println("b1=",b1) // 输出97
var b2 byte = 97
fmt.Printf("b2=%c\n",b2) // 输出a
b3 := '你'
fmt.Println("b2=",b3) // 输出unicode码值
下面测试字符和布尔类型分别占用几个字节。
b := 'a'
success := false
fmt.Println(unsafe.Sizeof(b)) // 输出4
fmt.Println(unsafe.Sizeof(success)) // 输出1
如果一个字符的UTF-8编码的字节数大于1,则不能用byte去存,而用int存就没问题。如下图所示。
2、字符串的基本使用
var s1 = "hello"+"world" // 拼接
s1+="hi"
// 由于go可以不以分号结尾,因此在字符串拼接语句太长需要换行时,每行的结尾必须以+号结尾
s2 := "我是"+"一个"+
"中国人"+"hello"+
"world"
s3 := `将字符串按原样输出,适用于输出源代码等。\n\t`
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s3)
3、基本数据类型之间的显示类型转换
var a int = 9999
var b byte = byte(a)
fmt.Println(b)
4、String和基本类型的相互转换
基本类型转string,第一种方法,如下代码所示。
var a int = 100
s := fmt.Sprintf("%d", a) // 使用Sprintf函数进行转换
fmt.Printf("type=%T,value=%q",s,s) // 输出s的类型和值,%q比%s的输出结果多一个双引号
基本类型转string,第二种方法,如下代码所示。
var a int = 100
s := strconv.FormatInt(int64(a), 2)
fmt.Printf("type=%T,value=%q",s,s)
// f表示格式,10表示小数位保留10位,64表示这个小数是float64
s = strconv.FormatFloat(12.99, 'f', 10, 64)
fmt.Printf("type=%T,value=%q",s,s)
string转为基本类型,如下代码所示。
b, err := strconv.ParseBool("fals1")
// b, _ := strconv.ParseBool("true") // 如果不想获取err信息则使用_
fmt.Printf("%v",b) // 如果不能转换成一个有效的值,则转为默认值,string转为其它类型也一样
fmt.Println(err)
4、获取用户输入
// 方式1
var name string
var age int
// fmt.Scanln(&name)
// fmt.Println("你的姓名是:",name)
// 方式2
fmt.Scanf("%s %d",&name,&age)
fmt.Println("你的姓名是:",name,",你的年龄是:",age)
5、生成随机数
// 设置种子
rand.Seed(time.Now().Unix())
n:=rand.Intn(100)+1
fmt.Println(n)
6、指针
// 输出基本类型的地址
i := 10
fmt.Println(&i)
// 声明一个指针,类型是*int,且p本身的值是&i
var p *int = &i
fmt.Println(p) // 输出p本身的值
fmt.Println(&p) // 输出p的地址
fmt.Println(*p) // 输出p指向的值
7、流程控制语句
1、if和switch
if 5>3 {
fmt.Println("你好")
}
// 支持在if中定义变量
if age:=10;age>9 {
fmt.Println("hello")
}
score:=30
switch score {
case 10,20: fmt.Println("差劲") // 自动带break
case 30,40: fmt.Println("还行"); fallthrough // 默认只穿透一层
default: fmt.Println("走3")
}
// 这种写法跟if-else差不多
switch {
case score<10 || score>90:fmt.Println("奇葩")
default:fmt.Println("中规中矩")
}
var service interface{}
var y = 10
service = y
switch service.(type) {
case nil:fmt.Println("空类型")
case int:fmt.Println("int类型")
case float64:fmt.Println("float64类型")
}
2、for循环
for i := 1; i < 10; i++ {
fmt.Println("hello")
}
// 相当于while循环,go中没有while关键字
j:=1
for j<10{
fmt.Println("hi")
j++
}
// 无限循环的两种写法
// for ;;{} 或 for{}
// for-range,可遍历字符串和数组
// 传统方式遍历字符串,按字节遍历,不能含中文
s:="hello,world中"
for i:=0;i<len(s);i++{
fmt.Printf("%c\n",s[i])
}
// for-range是按字符遍历的
for index,item:=range s{
fmt.Printf("%d-%c\n",index,item)
}
// 将string转为切片也可以实现按字符遍历
s2 := []rune(s)
for i:=0;i<len(s2);i++{
fmt.Printf("%c\n",s2[i])
}
3、goto语句
label:
fmt.Println("goto test1")
fmt.Println("goto test2")
goto label
8、函数
1、函数核心
语法:
func 函数名 (形参列表) (返回值类型列表){
}
注意点:
-
在接收多个返回值时,可以用占位符_忽略某个返回值。
-
如果返回值只有一个,返回值类型列表可以不写。
-
go函数不支持重载。
-
go函数的参数传递是值传递,如果希望在函数内修改变量值,则可以传入变量地址,在函数内用指针操作变量。
函数也是一种数据类型,可以赋值给变量,函数可以作为形参。如下代码所示。
// 定义匿名函数,并赋值给变量
mysum := func(a int,b int) int {
return a+b
}
fmt.Printf("调用mysum[%T][%d]",mysum,mysum(10,20))
自定义数据类型:type myint int、type mysum func(int,int) int,相当于取别名。如下代码所示。(这个语法用处不大)
type lensum func(s1 string,s2 string)int
var totallen lensum
totallen = func(s1 string,s2 string) int {
return len(s1)+len(s2)
}
fmt.Println("自定义类型:",totallen("hello","hi"))
也支持给函数返回值命名,如下代码所示。(这个语法用处不大)
func sum(a int,b int)(sum int,sub int){
sum = a+b
sub = a-b
return
}
可变参数:
// args是切片,可以通过索引访问
func sum2(args... int) int {
var sum int
for i := 0; i < len(args); i++ {
sum +=args[i]
}
return sum
}
初始化函数:
// 每个源文件都可以定义一个init函数,在main函数前执行,被go运行框架调用
func init(){
fmt.Println("Test_07_function.go初始化了")
}
函数闭包:
initClickCnt := func(initCnt int) func(int){
totalCnt := initCnt
// totalCnt和cnt构成了函数的闭包,totalCnt只初始化一次,相当于全局变量,而cnt相当于局部变量
return func(cnt int) {
totalCnt += cnt
fmt.Println("当前点击次数:",totalCnt)
}
}
printClickCnt := initClickCnt(100)
printClickCnt(8) // 108
printClickCnt(10) // 118
printClickCnt(2) // 120
defer延迟执行:
deferTest := func() {
// 相关语句进入defer栈中,待方法结束后,以后进先出的方式执行defer栈中的语句。可用于关闭文件句柄、数据库连接、锁等资源。
defer fmt.Println("关闭某资源")
defer fmt.Println("关闭某句柄")
fmt.Println("执行逻辑")
}
deferTest()
2、系统函数
时间和日期相关函数:
now := time.Now() // 获取当前时间
fmt.Printf("%02d-%02d-%02d %02d:%02d:%02d\n",now.Year(),now.Month(),now.Day(),now.Hour(),
now.Minute(),now.Second())
fmt.Println(now.Format("2006-01-02 15:04:05")) // 必须要这样写
time.Sleep(3*time.Second) // 休眠3秒
3、内置函数
len() 求字符串、数组长度
new() 用来分配内存,主要用来分配值类型(填充默认值),返回的是指针。如下代码所示。
p := new(int)
fmt.Printf("类型[%T]指针值[%v]指针地址[%v]指针指向的值[%v]指针指向的值类型[%T]",p,p,&p,*p,*p)
make() 用来分配内存,主要用来分配引用类型,返回的是指针
9、go语言规范
1、包规范
如果想要编译成一个可执行文件,需要有main包,如果只是写一个库,则可以不需要main包。
在gopath目录下执行如下命令编译成可执行文件:
// 编译时需要编译main包所在的文件夹
// 编译后生成的可执行文件再gopath目录下,也可以通过-o参数指定路径和文件名
go build -o bin/my.exe project_name/package_name/main
在import包时,路径从GOPATH/src开始写,不用带src。
在main包下面有多个main函数,在编译时如何忽略?
- 只需要在要忽略的main函数所在文件顶部加上// +build ignore 并且至少留一个空行即可。
2、标识符规范
包名尽量和目录名一致
如果变量名、函数名、常量名首字母大写,则为public,否则为private
10、go语言异常处理
func main() {
// 测试异常处理
test()
// 自定义错误
customError := func(s string) error{
if(s == "小明"){
return nil
}else{
return errors.New("参数值不是小明!")
}
}
if err := customError("小张");err!=nil {
panic(err)
}
}
func test() {
defer func() {
if err:=recover();err != nil{
fmt.Println("test方法发生了异常")
}
}() // 写()是为了要执行它
a:=10
b:=0
c:=a/b
fmt.Println(c)
}
11、数组与切片
1、数组
// 定义数组的几种方式
// 方式1
var arr [5]int
arr[0] = 2
fmt.Println(arr[0])
// 方式2
var arr2 = [5]int{1,2,3,4,5}
fmt.Println(arr2[0])
// 方式3
var arr3 = [...]int{1,2,3}
fmt.Println(arr3[0])
//方式4,如果写...则为数组类型,否则为切片
var arr4 = [...]string{1:"关羽",0:"刘备",2:"郑飞"}
fmt.Printf("%T",arr4)
2、切片
// 定义切片
// 方式1:引用数组。如果startIndex为0或endIndex为数组长度则可以省略,如arr[:]
var slice []int = arr2[1:4]
fmt.Println("切片的值:",slice)
fmt.Println("切片的元素个数:",len(slice))
fmt.Println("切片的容量:",cap(slice))
// 方式2
var slice2 []int = make([]int,4,16)
fmt.Println("切片的值:",slice2)
// 方式3:直接引用数组
var slice3 []int = []int{1,2,3,4,5}
fmt.Println("切片的值:",slice3)
// 切片动态增长
slice3 = append(slice3,6)
fmt.Println("切片的值:",slice3)
// 切片拷贝(数组不行)
var slice4 []int = make([]int,10,10)
copy(slice4,slice3)
fmt.Println(slice4)
3、string与slice
string底层是byte数组,因此也可以切片处理
// 字符串转成切片,也可以转成[]byte
s := "hello中国"
slice5 := []rune(s)
for _,item:=range slice5 {
fmt.Printf("%c",item)
}
4、多维数组
var arrtwo [3][4]int = [3][4]int{{1,2,3,4},{5,6,7,8},{9,10,11,12}}
fmt.Println("\n",arrtwo)
12、Map
key的类型:
- 可以的:bool、数字、string、指针、channel、接口、结构体、数组等
- 不可以:slice、map、function,因为这几个没法用==判断
// map可以动态扩容
var wordcnt map[string]int = make(map[string]int,10)
wordcnt["关羽"] = 20
// 声明时就初始化
var wordcnt2 map[string]int = map[string]int{
"张飞":20,
"赵云":16,
"曹操":32,
}
fmt.Println(wordcnt2)
//删除一个key,如果想删除所有key,则可以用make函数分配新内存
delete(wordcnt2,"张飞")
// 查找
value,b := wordcnt2["曹操"]
fmt.Println(value,b)
// 遍历,只能用for-range
for k,v:=range wordcnt2 {
fmt.Println(k,"=",v)
}
// map切片
var mapslice []map[string]int = make([]map[string]int,2)
// 动态增加
element := map[string]int{
"hello":5,
"ok":8,
}
mapslice = append(mapslice,element)
fmt.Println(mapslice)
13、go语言面向对象编程
1、结构体struct
注意点:
- struct与class类似。
- go面向对象编程非常简洁,没有继承、方法重载、构造函数、析构函数、隐藏的this指针等。
- go仍然有封装、继承、多态,只不过实现方式不一样,比如继承没有extends关键字,继承通过匿名字段(组合)实现。
- 面向接口编程是go很重要的特性。
- 结构体属性的地址是连续分配的
- 两个不同的结构体做类型转换时,需要对应的字段全部相同
- 可以给每个字段起一个tag,在序列化时会用到
- 由于go的方法作用在指定数据类型上,因此不仅是struct,int、float等也可以有方法
- 类型可以实现string方法(等同于Java中的toString方法)
type User struct {
name string `json:"name"`
age int `json:"age"`
password string `json:"password"`
}
func (u *User) addAge(b int) {
// 结构体是值类型,在参数传递时是值拷贝,因此不能修改原struct的属性
// 如果想修改原struct的属性,需要传入指针,即*User,且底层做了优化,可以用u.age代替(*u).age
u.age = u.age+b
}
func main() {
// 方式1:声明结构体变量并赋值
var u1 User = User{"张三",10,"123456"}
fmt.Println(u1)
// 方式2:
var u2 *User = new(User)
(*u2).name = "李四"
u2.password = "ok" // 这样写也可以是因为:在底层做了转换
fmt.Println(*u2)
// 方式3
var u3 *User = &User{}
u3.name="王五"
// 调用方法
u1.addAge(5)
fmt.Println(u1.age)
}
2、继承
继承通过匿名字段实现。
type Animal struct {
name string
age int
}
type Dog struct {
Animal
owner string
}
func main() {
dog := Dog{}
dog.owner = "张三"
dog.name = "拉布拉多"
dog.age = 1
fmt.Println(dog)
}
注意点:
- 可继承所有字段和方法,包括私有
- 当父子含有相同名称的字段或方法时次,采用就近原则
- 当多重继承时,且多个父结构体之间存在同名字段或方法,则必须指定是哪个父结构体
3、接口与多态
type UserService interface {
get() string
insert() int
}
type UserServiceImpl struct {
}
func (service UserServiceImpl) get() string {
fmt.Println("执行get方法")
return "name:张三;age:10"
}
func (service UserServiceImpl) insert() int {
fmt.Println("执行insert方法")
return 8
}
func doService(service UserService) {
fmt.Println(service.get())
fmt.Println(service.insert())
}
func main() {
doService(UserServiceImpl{})
}
注意点:
- go没有implements关键字
- 实现接口只需要一个自定义类型,然后实现接口里面的所有方法即可
- 空接口interface{}没有任何方法,因为认为所有类型实现了该接口,相当于Java总的Object类
- 接口中不能定义变量
- 接口可以继承
- go中多态是通过接口实现的,支持多态参数和多态数组
4、类型断言与强转的区别
// 类型断言与强转的区别
var b UserService
var a interface{} = UserServiceImpl{}
// 强转。报错,因为不知道a的具体类型,因此无法强转
// b = UserService(a) //
// 类型断言
b = a.(UserService)
doService(b)
// 带检测的类型断言
c,ok:=a.(UserService)
if ok {
doService(c)
}
14、文件操作
1、基本使用
// 打开文件
file, _ := os.OpenFile("F:/tmp/test.txt",os.O_RDONLY,os.ModePerm)
// 关闭文件
defer file.Close()
// 循环读取文件
reader := bufio.NewReader(file)
for{
line, err2 := reader.ReadString('\n')
if err2 == io.EOF {
// 表示读到了文件末尾
break
}
fmt.Println(line)
}
// 一次性读取文件
content, _ := ioutil.ReadFile("F:/tmp/test.txt")
fmt.Println(content)
// 写文件
writer := bufio.NewWriter(file)
writer.WriteString("hello")
writer.Flush()
// 判断文件是否存在
_, err := os.Stat("F:/tmp/test.txt")
if err == nil {
fmt.Println("存在")
}else if os.IsNotExist(err){
fmt.Println("不存在")
}else{
fmt.Println("不确定")
}
// 文件拷贝
io.Copy(writer,reader)
2、案例
1、分割文件
描述:将一个大文件分割为n个小文件
参数:
- inFilePath 大文件路径
- outFilePath 小文件存放目录
- splitLineNum 行号数组,如[2,4,6],会分别在大文件的第2、4、6行切割,最后生成4个小文件
func Split(inFilePath string,outFilePath string,splitLineNum []int) {
file, err := os.OpenFile(inFilePath, os.O_RDONLY, os.ModePerm)
suffix := path.Ext(inFilePath)
defer file.Close()
if nil != err{
fmt.Println("文件打开失败!")
return
}
reader := bufio.NewReader(file)
// 初始化变量
cnt := 0
index := 0
num := splitLineNum[index]
splitFile,_ := os.OpenFile(outFilePath+"/split_"+fmt.Sprintf("%d",index)+suffix, os.O_APPEND|os.O_CREATE, os.ModePerm)
writer := bufio.NewWriter(splitFile)
defer splitFile.Close()
for {
line, err := reader.ReadString('\n')
if io.EOF == err {
break
}
// 判断是够读到了分割线
if cnt == num {
// 一定要在文件close之前flush
writer.Flush()
splitFile.Close()
// 换下一个文件
index++
if index < len(splitLineNum) {
num = splitLineNum[index]
}
splitFile,_ = os.OpenFile(outFilePath+"/split_"+fmt.Sprintf("%d",index)+suffix, os.O_APPEND|os.O_CREATE, os.ModePerm)
writer = bufio.NewWriter(splitFile)
}
writer.WriteString(line)
cnt++
}
writer.Flush()
}
2、计算子文件大小
描述:输入一个路径,输出该路径下所有子文件的大小,默认按文件大小从大到小排序
参数:
- inFilePath 输入路径
- unit 单位
func calculateChildFileSize(inFilePath string) int64{
stat, _ := os.Stat(inFilePath)
file, _ := os.OpenFile(inFilePath, os.O_RDONLY, os.ModePerm)
defer file.Close()
if stat.IsDir() {
childs, _ := file.Readdir(-1)
var total int64
for _,item := range childs{
total += calculateChildFileSize(inFilePath + "/" + item.Name())
}
return total
}else{
return stat.Size()
}
}
func ChildFileSize(inFilePath string,unit string){
file, _ := os.OpenFile(inFilePath, os.O_RDONLY, os.ModePerm)
defer file.Close()
childs, _ := file.Readdir(-1)
var fileSizeMap map[int64]string = make(map[int64]string,10)
for _,item := range childs{
fileSizeMap[calculateChildFileSize(inFilePath+"/"+item.Name())] = item.Name()
}
var keys []int
for k,_ := range fileSizeMap {
keys = append(keys,int(k))
}
sort.Ints(keys)
for i:=len(keys)-1;i>=0;i--{
if unit == "KB" {
fmt.Println(fileSizeMap[int64(keys[i])],"\t",keys[i]/1024,"KB")
}else if unit == "MB" {
fmt.Println(fileSizeMap[int64(keys[i])],"\t",keys[i]/(1024*1024),"MB")
}else{
fmt.Println(fileSizeMap[int64(keys[i])],"\t",keys[i],"B")
}
}
}
3、文件同步
描述:将文件从一个地方同步到另一个地方,默认以覆盖方式同步,但目的地有而源头没有的文件不会被删除
参数:
- dst:目的地路径
- src:源文件或源文件夹
func Sync(dst string,src string) {
// 判断是否是文件夹,假设src是都存在的
stat, _ := os.Stat(src)
if !stat.IsDir() {
f1, _ := os.OpenFile(src, os.O_RDONLY, os.ModePerm)
if _, err := os.Stat(dst+"/"+path.Base(src));nil == err || os.IsExist(err){
// 如果存在,则删除之前的文件
os.Remove(dst+"/"+path.Base(src))
}
f2, _ := os.OpenFile(dst+"/"+path.Base(src), os.O_RDWR|os.O_CREATE, os.ModePerm)
defer f1.Close()
defer f2.Close()
io.Copy(f2,f1)
}else {
if _, err := os.Stat(dst+"/"+path.Base(src));nil != err && os.IsNotExist(err){
// 如果不存在,则创建文件夹
os.Mkdir(dst+"/"+path.Base(src),os.ModePerm)
}
f, _ := os.OpenFile(src, os.O_RDONLY, os.ModePerm)
childs, _ := f.Readdir(-1)
for _,item := range childs{
Sync(dst+"/"+path.Base(src),src+"/"+item.Name())
}
}
}
15、命令行参数
可以通过os.Args获取所有命令行参数。
更方便的是,可以指定参数名(如-name),在指定参数名的情况下,通过如下方式获取参数。
var name string
flag.Stringvar(&name,"name","默认值","用户名")
flag.Parse()
16、序列化与反序列化
type Student struct {
Name string `json:"name"`
Age int `json:"age"`
Score int `json:"score"`
}
// 序列化
stu := Student{"张三",23,98}
jsonStr, _ := json.Marshal(stu)
fmt.Println(string(jsonStr))
// 反序列化
var stu2 Student
_ = json.Unmarshal([]byte(jsonStr), &stu2)
fmt.Println(stu2)
17、goroutine与channel
1、goroutine概述
go可以轻轻松松起上万个协程,原因是在底层做了优化,可以理解为更轻量级的线程
go协程的特点:
- 有独立栈空间
- 共享程序堆空间
- 调度由用户控制
如果主线程退出了,则协程即使还没有执行完毕,也会退出。
主线程是一个物理线程,直接作用在CPU上,是重量级的,非常耗CPU资源。
协程是从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小。
其它编程语言是基于线程的,开启过多的线程资源耗费大,这一点go的并发优势就很大。
2、goroutine并发模型
MPG模式:
- M:操作系统的主线程(是物理线程)
- P:协程执行需要的上下文
- G:协程
一个程序可以有多个M,当多个M在不同的CPU上运行就是并行。
每个M对应一个P、一个正在运行的G和一个协程等待队列。
如何开启一个协程?
go test()
3、并发安全
当有并发安全问题时,编译带-race参数,然后在执行exe文件,结果是正确的。
如何保证线程安全:
-
锁
var lock sync.Mutex lock.Lock() lock.Unlock()
-
channel(不同goroutine之间进行通讯)
4、channel概述
channel本质就是一个队列。
多goroutine访问channel时,不需要加锁,channel本身是线程安全的。
channel是有类型的,string channel只能存放string类型。
channel的基本使用:
// 声明channel
var intChan chan int
intChan = make(chan int,10)
// 写数据,写多了就报错
intChan<-10
intChan<-20
intChan<-30
intChan<-40
// 这个容量不会动态增长
fmt.Println(len(intChan),cap(intChan))
// 读数据,读多了就报错
ele := <-intChan
fmt.Println(ele)
// 关闭管道,关闭之后只能读不能写
close(intChan)
// channel可以遍历,但如果channel没有关闭,则会报错,否则正常遍历
for item:=range intChan{
fmt.Println(item)
}
注意事项:
- channel可以声明为只读或只写。
- 使用select可以解决从管道读数据的阻塞问题。
- 在goroutine中使用recover,可以避免goroutine因panic而退出。
18、反射
通过反射(reflect.TypeOf函数)可以获取到变量的类型(reflect.Type接口)。
通过反射(reflect.ValueOf函数)可以获取到变量的值(reflect.Value结构体)。
19、网络编程
func server() {
server, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
defer server.Close()
fmt.Println("服务端启动成功!")
for {
client, _ := server.Accept()
fmt.Println("客户端连接成功!")
var data []byte = make([]byte,128)
client.Read(data)
fmt.Println("服务端接收到消息:",string(data))
}
}
func client() {
client, _ := net.Dial("tcp", "localhost:8888")
defer client.Close()
data := []byte("你好,golang!")
client.Write(data)
}
func main() {
go server()
time.Sleep(1*time.Second)
client()
}