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、变量使用方式

  1. 指定变量类型,声明后若不赋值,使用默认值
  2. 根据值自动推导类型
  3. 连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 intfmt.Println("i=",i,"j=",j,"k=",k)var o,p,q = 10,"hello",10.5fmt.Println("o=",o,"p=",p,"q=",q)r,s,t := 10,"hello",10.5fmt.Println("r=",r,"s=",s,"t=",t)

3、声明全局变量

package mainvar n1 = 100
var n2 = "okk"
var (n3 = 36.9n4 = "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) // 输出97var b2 byte = 97fmt.Printf("b2=%c\n",b2) // 输出ab3 := '你'fmt.Println("b2=",b3) // 输出unicode码值

下面测试字符和布尔类型分别占用几个字节。

b := 'a'
success := falsefmt.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 = 9999var b byte = byte(a)fmt.Println(b)

4、String和基本类型的相互转换

基本类型转string,第一种方法,如下代码所示。

    var a int = 100s := fmt.Sprintf("%d", a) // 使用Sprintf函数进行转换fmt.Printf("type=%T,value=%q",s,s) // 输出s的类型和值,%q比%s的输出结果多一个双引号

基本类型转string,第二种方法,如下代码所示。

    var a int = 100s := strconv.FormatInt(int64(a), 2)fmt.Printf("type=%T,value=%q",s,s)// f表示格式,10表示小数位保留10位,64表示这个小数是float64s = 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、获取用户输入
	// 方式1var name stringvar age int// fmt.Scanln(&name)// fmt.Println("你的姓名是:",name)// 方式2fmt.Scanf("%s %d",&name,&age)fmt.Println("你的姓名是:",name,",你的年龄是:",age)
5、生成随机数
	// 设置种子rand.Seed(time.Now().Unix())n:=rand.Intn(100)+1fmt.Println(n)
6、指针
    // 输出基本类型的地址i := 10fmt.Println(&i)// 声明一个指针,类型是*int,且p本身的值是&ivar p *int = &ifmt.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:=30switch score {case 10,20: fmt.Println("差劲") // 自动带breakcase 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 = 10service = yswitch 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:=1for 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)intvar totallen lensumtotallen = 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+bsub = a-breturn
}

可变参数:

// args是切片,可以通过索引访问
func sum2(args... int) int {var sum intfor 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 += cntfmt.Println("当前点击次数:",totalCnt)}}printClickCnt := initClickCnt(100)printClickCnt(8) // 108printClickCnt(10) // 118printClickCnt(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:=10b:=0c:=a/bfmt.Println(c)
}
11、数组与切片

1、数组

	// 定义数组的几种方式// 方式1var arr [5]intarr[0] = 2fmt.Println(arr[0])// 方式2var arr2 = [5]int{1,2,3,4,5}fmt.Println(arr2[0])// 方式3var 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))// 方式2var 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数组,因此也可以切片处理

	// 字符串转成切片,也可以转成[]bytes := "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-rangefor 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).ageu.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)// 方式3var u3 *User = &User{}u3.name="王五"// 调用方法u1.addAge(5)fmt.Println(u1.age)
}

2、继承

继承通过匿名字段实现。

type Animal struct {name stringage int
}
type Dog struct {Animalowner string
}func main() {dog := Dog{}dog.owner = "张三"dog.name = "拉布拉多"dog.age = 1fmt.Println(dog)
}

注意点:

  • 可继承所有字段和方法,包括私有
  • 当父子含有相同名称的字段或方法时次,采用就近原则
  • 当多重继承时,且多个父结构体之间存在同名字段或方法,则必须指定是哪个父结构体

3、接口与多态

type UserService interface {get() stringinsert() 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 UserServicevar 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 := 0index := 0num := 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之前flushwriter.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 int64for _,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 []intfor 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()

4、channel概述

channel本质就是一个队列。

多goroutine访问channel时,不需要加锁,channel本身是线程安全的。

channel是有类型的,string channel只能存放string类型。

channel的基本使用:

	// 声明channelvar intChan chan intintChan = make(chan int,10)// 写数据,写多了就报错intChan<-10intChan<-20intChan<-30intChan<-40// 这个容量不会动态增长fmt.Println(len(intChan),cap(intChan))// 读数据,读多了就报错ele := <-intChanfmt.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()
}