go 学习笔记

go 学习笔记,第1张

go 学习笔记(七)

文章目录 go 学习笔记(七)前言一、channel(管道)二、反射三、网络编程四、数据结构总结


前言

接触了新的语言go,记录一下学习的笔记方便日后温故知新。

一、channel(管道)

1.channel的本质就是数据结构:队列
2.数据是先进先出
3.线程安全,channel本身是线程安全的
4.channel是有类型的,string的channel只能放string,结构体类型的channel只能放结构体
5.快速入门
使用:
初始化一个可以存放三个int类型的channel

	func main() {
	//初始化一个可以存放三个int类型的channel
	var intChan chan int
	//管道的容量和map不一样,管道的容量不能自动扩容,所以要随用随取,及时取出管道里面的数据,存取数据遵从先进先出原则
	intChan = make(chan int, 3)
	fmt.Printf("intChan的值是 %v,intChan的地址是 %p \n", intChan, &intChan)
	//向管道中追加数据
	intChan <- 10
	num := 3
	intChan <- num
	fmt.Printf("intChan len is %v, intChan cap is %v \n", len(intChan), cap(intChan))

	//在没有使用协程的情况下,如果管道的数据取完了,再取就会报错deadlock
	num2 := <-intChan
	fmt.Println("num2 = ", num2)
}

6.管道的关闭
使用内置的函数 close 可以关闭管道,当管道关闭后,这个管道就只能读不能写了

func main() {
	intChan := make(chan int, 3)
	intChan <- 10
	intChan <- 20
	close(intChan) //panic: send on closed channel
	intChan <- 30
}

7.管道的遍历

func main() {
	intChan := make(chan int, 3)
	intChan <- 10
	intChan <- 20
	//在遍历channel时.如果没关闭channel则会出现死锁,只有关闭channel之后才能正常遍历,并且遍历管道不能使用经典for,需使用 foreach
	close(intChan) //panic: send on closed channel
	fmt.Println("before for intChan len is ", len(intChan))
	for v := range intChan {
		fmt.Println(v)
	}
	fmt.Println("after for intChan len is ", len(intChan))
	//before foreach intChan len is  2
	//10
	//20
	//after foreach intChan len is  0
}

8.使用管道解决协程的资源竞争问题
1.开启一个协程,向管道中写入50个整数,并开启另一个协程来读取上一个协程写入的整数,两个协程 *** 作同一个管道,主线程等两个协程结束后,退出
(数据量更改为1000并开启10个逻辑cpu才能看到并行执行的效果)
思路:
两个管道,其中一个管道由协程不停的读写,另一个管道做标志位来阻塞主线程

	func writeData(intChan chan int) {
	for i := 0; i < 1000; i++ {
		//放入数据
		intChan <- i
		fmt.Println("写入数据 = ", i)
	}
	//写完之后关闭管道,一定要关闭否则读的时候会死锁
	close(intChan)
}
func readData(intChan, exitChan chan int) {
	for {
		//如果还存在数据 ok  就是true
		v, ok := <-intChan
		if ok {
			fmt.Println("读取到数据 : ", v)
		} else {
			break
		}
	}
	//任务完成
	exitChan <- 1
	close(exitChan)
}
func main() {
	runtime.GOMAXPROCS(10)
	intChan := make(chan int, 1000)
	exitChan := make(chan int, 1)
	go readData(intChan, exitChan)
	go writeData(intChan)
	for {
		v := <-exitChan
		if v == 1 {
			break
		}
	}
}

小练习 启动一个写协程写入 1~2000个数字,然后启动8个读线程,计算取出的数字的累加,计算后把结果以map的形式放入到管道当中最后遍历这个管道

func write(writeChan chan int) {
	for i := 1; i <= 200; i++ {
		writeChan <- i
	}
	close(writeChan)
}
func read(readChan chan int, exitChan chan int, numChan chan map[int]int) {
	for {
		num, ok := <-readChan
		res := 0
		if ok {
			for i := 1; i <= num; i++ {
				res += i
			}
			m := make(map[int]int)
			m[num] = res
			numChan <- m
		} else {
			v := <-exitChan
			if v == 1 {
				close(numChan)
			}
			break
		}
	}
}
func main() {
	//小练习 启动一个写协程写入 1~2000个数字,然后启动8个读线程,计算取出的数字的累加,计算后把结果以map的形式放入到管道当中最后遍历这个管道
	intChan := make(chan int, 200)
	exitChan := make(chan int, 1)
	numChan := make(chan map[int]int, 200)
	go write(intChan)
	for i := 0; i < 8; i++ {
		go read(intChan, exitChan, numChan)
	}
	for {
		if len(numChan) == 200 {
			exitChan <- 1
			break
		}
	}
	fmt.Println("numChan is finish the len is ", len(numChan))
	for m := range numChan {
		fmt.Println(m)
	}
	fmt.Println("numChan has foreach the len is ", len(numChan))

}

9.管道的阻塞
当管道的容量小于数据的量的时候,会产生阻塞,但如果是边写边读的话,go分析有协程大量的写,即便只有协程读的很慢,也不会报错
如果编译器发现一个管道只有写没有读则会发生阻塞,如果管道的读写频率不一致是可以正常跑的
10.计算1~20000哪些是素数
传统方法,使用一个循环,循环判断是不是素数
使用并发/并行来计算将计算素数的任务分配给多个协程去完成
思路:
建立一个管道放入数据
写一个函数,入参为管道,取出一个数,看是否是素数,是的话就放入另一个管道,直到取不出数据的时候,放flag管道一个标志
设置多核然后for cpuNum - 1 来执行求素数函数
写一个函数,入参是管道,for 判断管道的len,当管道的len == cpuNum - 1 时候,break 所有协程,打印素数,退出main

//	获取素数
//	num := 100
//flag:
//	for i := 2; i <= num; i++ {
//		for j := 2; j < i; j++ {
//			if i%j == 0 {
//				continue flag
//			}
//		}
//	}

计算1~20000哪些是素数:

//这个协程放入需要处理的数据
//func putNum(intChan chan int) {
//	for i := 0; i < 100000; i++ {
//		intChan <- i
//	}
//	close(intChan)
//}

//获取素数的协程
func primeNumberFunc(intChan, primeNumber chan int, flagChan chan bool) {
	for {
		num, ok := <-intChan
		if !ok {
			break
		}
		isPrimeNumber := false
		for i := 2; i < num; i++ {
			if num%i == 0 {
				//不是素数
				isPrimeNumber = true
				break
			}
		}
		if !isPrimeNumber {
			//是素数
			primeNumber <- num
		}
	}
	//跳出循环,已经取不到数据了
	flagChan <- true
}

func main() {
	//计算1~20000哪些是素数
	//数据管道
	intChan := make(chan int, 1000)
	//结果管道
	primeNumber := make(chan int, 2000)
	//标志位管道
	flagChan := make(chan bool, 4)

	start := time.Now().Unix()
	//go putNum(intChan)
	for i := 2; i < 1000; i++ {
		intChan <- i
	}
	close(intChan)

	for i := 0; i < 4; i++ {
		go primeNumberFunc(intChan, primeNumber, flagChan)
	}
	//主线程阻塞等待<-flagChan管道可以取出四个值的时候完成循环否则就一直阻塞
	go func() {
		for i := 0; i < 4; i++ {
			<-flagChan
		}
		end := time.Now().Unix()
		fmt.Println("消耗时间为:", end-start)
		close(primeNumber)
	}()

	for {
		res, ok := <-primeNumber
		if !ok {
			break
		}
		fmt.Println("素数有:", res)
	}
	fmt.Println("主线程退出")
}

结论:
使用go协程后,效率至少提高 cpu数量的倍数
管道的注意事项:
管道可以声明为只读或只写

func main() {
	//默认情况下管道是双向的,可读可写
	//使用场景,此时管道可以作为方法的入参只允许方法对这个管道读或写,效率上更好并且可以防止误 *** 作,是管道的一种属性,我们可以把双向的通道传递到只读或只写的入参
	//声明只写管道
	var writeChan chan<- int
	writeChan = make(chan int, 1)
	writeChan <- 1
	//声明只读管道
	var readChan <-chan int
	readChan = make(chan int, 1)
	<-readChan
}
//使用select可以解决从管道中取数据而不用关闭管道的问题
	func main() {
		intChan := make(chan int, 10)
		for i := 0; i < 10; i++ {
			intChan <- i
		}
		stringChan := make(chan string, 5)
		for i := 0; i < 5; i++ {
			stringChan <- "hello" + fmt.Sprintf("%d", i)
		}
		//传统方法遍历管道如果不关闭会阻塞而导致死锁deadlock,当我们不确定什么时候关闭管道的时候,我们可以用select
		//flag:
		for {
			select {
			//如果我们取不到数据了,会向下继续执行case
			case v := <-intChan:
				fmt.Println("我们在管道未关闭的情况下取到了数据:", v)
			case v := <-stringChan:
				fmt.Println("我们在管道未关闭的情况下取到了数据:", v)
			default:
				fmt.Println("什么都取不到了,执行自己的业务逻辑,比如 return 或 break 退出当前函数,目前是在main函数,那么就是退出程序了")
				return
				//break flag
			}
		}
		//如果使用return,则fmt.Println("hahaha ") 永远不可达
		fmt.Println("hahaha ")
	}
//在协程中捕获 panic 保证程序可以继续执行
func sayHello() {
	for i := 0; i < 10; i++ {
		time.Sleep(time.Second)
		fmt.Println("hello world")
	}
}
func test() {
	//panic: assignment to entry in nil map
	//触发panic,未对map进行make就使用,最终会影响main函数运行,这里加入 defer 和 recover 来解决
	//var myMap map[int]string
	//myMap[1] = "hello"

	defer func() {
		//捕获test()抛出的panic
		if err := recover(); err != nil {
			fmt.Println("func test() panic is", err)
		}
	}()
	var myMap map[int]string
	myMap[1] = "hello"

}
func main() {
	go sayHello()
	go test()
	for i := 0; i < 10; i++ {
		time.Sleep(time.Second)
		fmt.Println("main() is ok ...")
	}
}

//func test() panic is  assignment to entry in nil map
//main() is ok ...
//hello world
//hello world
二、反射

1.对基本数据类型的反射 *** 作

//适配器函数
//package reflect
//import "reflect"
//
//reflect包实现了运行时反射,允许程序 *** 作任意类型的对象。典型用法是用静态类型interface{}保存一个值,通过调用TypeOf获取其动态类型信息,该函数返回一个Type类型值。调用ValueOf函数返回一个Value类型值,该值代表运行时的数据。Zero接受一个Type类型参数并返回一个代表该类型零值的Value类型值。
//
//参见"The Laws of Reflection"获取go反射的介绍:http://golang.org/doc/articles/laws_of_reflection.html

//专门演示反射
func reflectTest01(b interface{}) {
	//通过入参获取 type kind 值
	//先获取到 reflect.Type()
	rType := reflect.TypeOf(b)     //rType本质是一个接口
	fmt.Println("rType = ", rType) //rType =  int

	//获取reflect.ValueOf()
	rValue := reflect.ValueOf(b)
	fmt.Println("rValue = ", rValue)               //rValue =  100 但是此时 100 的类型并不是int,所以不能当成int来用(不能来做运算) ,可以使用文档type Value的部分函数来获取真正的值
	fmt.Printf("the rValue type is %T \n", rValue) //the rValue type is reflect.Value
	fmt.Println("rValue's is = ", rValue.Int()+1)  //以反射获取的值获取真正的值来运算 rValue's is =  101

	//将rValue转成interface{}类型
	iv := rValue.Interface()
	//将interface{}通过断言转成需要的类型
	num2 := iv.(int)
	fmt.Println("num2 = ", num2)
}
func main() {
	num := 100
	reflectTest01(num)
}

2.对结构体的反射演示

func reflectTest02(b interface{}) {
	//通过入参获取 type kind 值
	//先获取到 reflect.Type()
	rType := reflect.TypeOf(b)     //rType本质是一个接口
	fmt.Println("rType = ", rType) //rType =  main.student
	//获取reflect.ValueOf()
	rValue := reflect.ValueOf(b)
	fmt.Println("rValue = ", rValue)               //rValue =  {张三 12}
	fmt.Printf("the rValue type is %T \n", rValue) //the rValue type is reflect.Value
	//下面将rValue转成interface{}

	iv := rValue.Interface()
	fmt.Printf("the iv type is %T \n", iv) //the iv type is main.student,但是此时无法取出stu的值,还需要对接口进行断言才行.因为是在运行时获取的,编译时无法确定iv是stu
	stu, ok := iv.(student)
	if ok {
		fmt.Println("stu.Name = ", stu.Name) //stu.Name =  张三
	}
	//.因为是在运行时获取的,编译时无法确定iv是stu,所以可能存在多个相同属性的结构体,其实可以使用switch的形式来完成多种形式的断言
}

//专门演示反射
type student struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	stu := student{
		Name: "张三",
		Age:  12,
	}
	reflectTest02(stu)
}

3.注意细节

reflect.Value.Kind 获取变量的类别,返回的是一个常量
type student struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {
	stu := student{
		Name: "张三",
		Age:  12,
	}
	kind1 := reflect.TypeOf(stu).Kind()
	kind2 := reflect.ValueOf(stu).Kind()
	fmt.Println(kind1) //struct
	fmt.Println(kind2) //struct
}

4.定义常量
只能定于基本数据类型

func main() {
	const name = "张三"
	const (
		name1 = "李四"
		age   = 12
	)

	const (
		a = iota //iota = 0 如果下面的没有显式的赋值或者赋值 iota 那么就是 iota ++
		b
		c
		d
	)
	const (
		q    = iota       //	当前是0
		w    = iota       //换行后是1
		e, r = iota, iota //换行后是2 两个值一行赋值就是上面的值累加下的
	)

	fmt.Println(name)  //张三
	fmt.Println(name1) //李四
	fmt.Println(age)   //12
	fmt.Println(a)     //0
	fmt.Println(b)     //1
	fmt.Println(c)     //2
	fmt.Println(q)     //0
	fmt.Println(w)     //1
	fmt.Println(e)     //2
	fmt.Println(r)     //2
}
func main() {
	const (
		age = "阿松大"
		hi
		how
	)
	fmt.Println(age) //阿松大
	fmt.Println(hi)  //阿松大
	fmt.Println(how) //阿松大
}

5.type是类型,kind是类别两者可能相同也可能不同
比如不同包下的相同结构体,type是 pkg1.Person 和 pkg2.Person 但kind都是struct
可以总结为,如果是基本数据类型的话.kind和type是一样的,因为不存在不同包的问题,结构体的话因为type是 包名.结构体名称 所以type可能是不同的,当然了kind都是结构体

通过反射可以让变量在interface{}和Reflect.Value直接互相转换
变量 <–> interface{} <–> Reflect.Value

通过反射的方式获取变量的值(并返回对应的类型),要求数据类型匹配
比如 x 是 int,那么就应该用 Reflect.Value(x).int(),而不能用其他的,否则会报panic

Elem() 获取 反射指针指向的值

func changeValue(b interface{}) {
		//此时想修改b的值
		value := reflect.ValueOf(b)
		fmt.Println("value's kind is ", value.Kind()) //如果	changeValue(number) 那么返回 value's kind is int , 是无法修改这个变量的值的,所以必须传入地址
		value.Elem().SetInt(20)                       //所以要通过方法.Elem()获取到指针所指向的值然后调用set方法来实现通过反射修改值的 *** 作,可以实现传递地址然后通过*来修改的效果
	}
	func main() {
		//此时想修改number的值,则需要对方法传入指针
		number := 10
		fmt.Println("before change number is", number)
		changeValue(&number)
		fmt.Println("after change number is", number)
		//before change number is 10
		//value's kind is  ptr
		//after change number is 20
	}

实践
通过反射来遍历结构体字段,调用结构体方法,获取结构体标签的值

func main() {
		mon := Monster{
			Name:   "牛魔王",
			Age:    20,
			Score:  100.00,
			gender: "男",
		}
		TestStruct(mon)
	}

	type Monster struct {
		Name   string `json:"name"`
		Age    int    `json:"monster_age"`
		Score  float64
		gender string
	}
	func (m Monster) Print() {
		fmt.Println("---start---")
		fmt.Println(m)
		fmt.Println("---end---")
	}
	func (m Monster) GetSum(n1, n2 int) int {
		return n1 + n2
	}
	func (m Monster) Set(name string, age int, score float64, gender string) {
		m.Name = name
		m.Age = age
		m.gender = gender
		m.Score = score
	}
	func TestStruct(b interface{}) {
		//获取 reflect.Type 类型
		typ := reflect.TypeOf(b)
		// 获取 reflect.Value 类型
		val := reflect.ValueOf(b)
		//通过 reflect.Value 获取 b 对应的类别
		kd := val.Kind()
		//如果不是结构体就退出函数,不玩了!
		if kd != reflect.Struct {
			fmt.Println("expect struct")
			return
		}
		//获取该结构体下有几个字段
		fields := val.NumField()
		fmt.Printf("this Struct has %d fields\n", fields) //this Struct has 4 fields

		//遍历结构体字段
		for i := 0; i < fields; i++ {
			fmt.Printf("field %d is %v\n", i, val.Field(i))
			//field 0 is 牛魔王
			//field 1 is 20
			//field 2 is 100
			//field 3 is 男
		}
		//遍历结构体tag
		for i := 0; i < fields; i++ {
			//获取Struct的tag,此时需要注意,获取标签需要使用reflect.Type来获取
			tagVal := typ.Field(i).Tag.Get("json")
			if tagVal != "" {
				fmt.Printf("field val is %v and tag is %v\n", val.Field(i), tagVal)
				//field val is 牛魔王 and tag is name
				//field val is 20 and tag is monster_age
			}
		}
		//获取结构体有多少个方法
		methods := val.NumMethod()
		fmt.Printf("struct has %d methods\n", methods) //struct has 3 methods 但是 val.NumMethod() 只统计公有方法,私有方法(开头字母小写的方法)不纳入统计
		//通过反射调用方法,获取第二个方法并调用并传入参数nil,获取方法就可以通过反射对方法命名进行规范化,比如test框架
		val.Method(1).Call(nil)
		//---start---
		//{牛魔王 20 100 男}
		//---end---
		//调用的顺序是采用函数名的ASCII码顺序来确定调用顺序的,而不是声明顺序
		//---
		//调用结构体第一个方法 Method(0)
		//构造一个结构体value切片
		var params []reflect.Value //声明了[]reflect.Value
		//将10转为reflect.Value类型并append到切片params
		params = append(params, reflect.ValueOf(10)) //func ValueOf(i interface{}) Value { ...} 接收空接口返回value类型的数据
		params = append(params, reflect.ValueOf(40))
		callValue := val.Method(0).Call(params)
		for _, value := range callValue {
			fmt.Println(value) //50

		}
		fmt.Println("callValue is", callValue[0].Int()) //50
	}
三、网络编程

1.TCP socket编程,是网络编程的主流之所以叫Tcp socket是因为底层是基于 TCP/IP 协议的,比如 qq聊天
2.b/s 架构,基于http,而http也是基于 TCP/IP的
3.网络编程基础知识:

osi模型(理论):
	应用层
	表示层
	会话层
	传输层
	网络层
	数据链路层
	物理层
Tcp/Ip模型(实际):
应用层(application,smtp,ftp,telnet,http)
传输层:解释数据
网络层:(ip)定位,ip地址和确定链接路径
链路层:(link):与硬件驱动对话
cmd追踪tracert www.baidu.com

每个internet上的主机和路由器都有一个ip地址,他包括网络号和主机号.ip地址有ipv4(32位)和ipv6(128位),可以通过ipconfig查看

只要是做程序的服务必须留一个监听端口,交互(通讯)的通道,服务断的通讯端口是唯一的,但客户端可以是随意的端口与其进行交互

端口
这里指的端口不是物理端口,指的是tcp/ip协议中的端口,指的是逻辑意义上的端口,如果把ip比作一间房子,端口就是进入这间房子的门
但是一股ip地址的端口可以有65536个(即 256 * 256)个之多,端口是通过端口号来标记的,端口号只有整数范围是 0~65536(256 * 256-1)

0 是保留端口
1-1024是保留端口,程序员一般不使用,比如22:ssh,23:telnet,21:ftp,25:sftp,80:iis,7:echo服务
1025-65535是动态端口,是程序员可以编程时候使用

在计算机尤其是服务器,要尽可能的少开端口
一个程序只能监听一个端口
如果使用netstat-an可以查看本机有哪些端口在监听
可以使用netstat-ant查看监听端口的pid,在结合任务管理器关闭不必要的端口

四、数据结构

1,队列(数组模拟环形队列)

func main() {
	//测试基于数组实现的环形队列,插入 0~4
	const size = 5
	queue := NewCircleQueue(size)
	for i := 0; i < size; i++ {
		err := queue.PushCircleQueue(fmt.Sprint("hello", i))
		if err != nil {
			fmt.Println(err)
			return
		}
		queue.PopCircleQueue()
	}
	queue.PushCircleQueue("hi")
	arr := queue.ListCircleQueue()
	for _, v := range arr {
		fmt.Println(v)
	}
}

// NewCircleQueue 创建环形队列的方法
func NewCircleQueue(size int) (c *CircleQueue) {
	queue := CircleQueue{
		maxSize: size,
		array:   make([]string, size),
		head:    0,
		tail:    0,
	}
	c = &queue
	return
}

// ListCircleQueue 遍历队列内容
func (c *CircleQueue) ListCircleQueue() []string {
	return c.array[c.head%c.maxSize : c.tail%c.maxSize]
}

// PushCircleQueue 添加入队列的方法
func (c *CircleQueue) PushCircleQueue(val string) (err error) {
	//入参钱判断队伍是不是已经满了
	if c.IsFull() {
		err = errors.New("PushCircleQueue err , Queue is full")
	}
	//没有满,将入参加入数组的指向队尾的位置tail并将tail ++
	c.array[c.tail%c.maxSize] = val
	c.tail++
	return
}

// PopCircleQueue d出队列的方法
func (c *CircleQueue) PopCircleQueue() (val string, err error) {
	//先判断队列是否已经空了
	if c.IsEmpty() {
		err = errors.New(" PopCircleQueue err , Queue is empty")
	}
	//没有空,可以取出元素,将元素的第一个取出赋值给val,并返回
	val = c.array[c.head%c.maxSize]
	//将头部索引向后移动
	c.head++
	return
}

// IsFull 当头尾两个元素补相等时候,进行取模 *** 作,当内置切片填充到第下标为4的时候,此时push方法中c.tail++执行后,c.tail等于构造函数入参size,对maxSize取模为0,切片已达size
func (c *CircleQueue) IsFull() bool {
	return !c.IsEmpty() && (c.tail-c.head)%c.maxSize == 0
}

//IsEmpty 判断环形队列空了,头就是尾,尾就是头,重叠了,没有元素了,就空了
func (c *CircleQueue) IsEmpty() bool {
	return c.head == c.tail
}

//判断环形队列中有多少元素
func (c *CircleQueue) size() int {
	//如果是空队列直接返回0即可
	if c.IsEmpty() {
		return 0
	}
	return (c.tail - c.head) % (c.maxSize + 1)
}

// CircleQueue 使用一个结构体管理环形队列
type CircleQueue struct {
	maxSize int      //最大容量len(array)
	array   []string //切片
	head    int      //指向队列头部
	tail    int      //指向队列尾部
}

2.链表
链表是有序的列表,在内存中的地址可能不是连续的
单链表:
为了比较好的对单链表进行crud *** 作,一般会给链表设置一个头节点,这个节点不存放数据
实践:

func main() {
	//创建头节点
	head := HeroNode{
		no:   0,
		name: "头节点",
	}

	//创建一个新节点
	hero1 := HeroNode{
		no:       1,
		name:     "宋江",
		nickName: "及时雨",
	}
	//创建一个新节点
	hero2 := HeroNode{
		no:       2,
		name:     "卢俊义",
		nickName: "玉麒麟",
	}

	insert(&head, &hero1)
	insert(&head, &hero2)
	ListLink(&head)
}

//编写第一种插入方式,直接在链表最后加入
func insert(herd, newNode *HeroNode) {
	//先找到链表的最后节点
	//创建一个临时节点
	temp := herd
	for {
		if temp.next == nil { //表示找到最后一个
			break
		}
		temp = temp.next //移动指向
	}
	//遍历结束 temp 为当前链表的最后一个
	temp.next = newNode
}

// ListLink 展示列表
func ListLink(head *HeroNode) {
	temp := head
	//先判断列表是不是空的
	if temp.next == nil {
		fmt.Println("链表为空")
		return
	}
	//遍历链表,因为链表不是空链表,所以采用do while 方式循环往复遍历
	for {
		//头节点不是最后一个节点,因为我们没有给头节点赋值,所以直接从第二个节点开始遍历
		fmt.Printf("[ %d , %s ,%s] == >", temp.next.no, temp.next.name, temp.next.nickName)
		temp = temp.next
		if temp.next == nil {
			fmt.Println("遍历结束!")
			break
		}
	}
}

// HeroNode 定义一个node
type HeroNode struct {
	no       int
	name     string
	nickName string
	next     *HeroNode //指向下一个节点

}
总结

对go基础的学习暂时先到这里,下一步是学习go-micro,并对go和java的微服务进行整合,项目和学习的心得会分享出来,还是挂一张好看的图

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/994522.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存