go

最详细的 Go学习笔记总结

Just Do It
2022-06-07 / 0 评论 / 73 阅读 / 正在检测是否收录...

第1章 Go介绍

package main
import "fmt"
func main()  {
	fmt.Println("hello word")
}

第2章 Go基本语法

2.1变量

2.1.1. go语言中变量分为局部变量和全局变量

  • 局部变量,是定义在打括号{}内部的变量,打括号内部也是局部变量的作用域
  • 全局变量,是定义在函数和打括号外部{}的变量

2.1.2. 变量声明

格式:
var  变量名 变量类型

批量声明未初始化的变量

var {
    a int
    b string
    c []float32
    e struct {
        x int
        y string
    }
}

初始化变量

var a int = 20 #标准声明格式
var b = 30 #自动推断类型格式
c := 40 #初始化声明格式,首选

2.1.3.变量多重赋值

以简单算法交换为例,传统写法如下

var a int = 10
var b int = 20
b,a = a,b


2.1.4.匿名变量

Go语言中的函数可以返回多个值,而事实上并不是所有返回值都用的上,那么就可以用匿名变量 “_” 替换即可,匿名变量不占用命名空间,也不会分配内存

func GetData()(int,int){
    return 10,20
}
a,_ := GetData() //舍弃第二个返回值
_,b = GetData()//舍弃第一个返回值

2.2 数据类型

2.3 打印格式化

2.4 数据类型转换

Go语言采用数据类型前置加括号的方式进行类型转换,格式如:T(表达式)。T表示要转换的类型

a := 10

b := string(a) //将int型转换为string型
c := float32(a) //将int型转换为float型

2.5 常量

相对于变量,常量是不变的值。 常量是一个简单的标识符,在程序运行时,不会被修改

格式如下:
const   标识符 [类型] = 值
const PAI string = "abc"

2.5.1 常量用于枚举

const (
	USERNAME = "geinihua"
	PASSWORD = "geinihua"
	NETWORK = "tcp"
	SERVER = "10.247.22.146"
	PORT = "3306"
	DATABASE = "demo"
)
	dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s",USERNAME,PASSWORD,NETWORK,SERVER,PORT,DATABASE)


常量组中如果不指定类型和初始值,则与上一行非空常量值相同

const (
    a=10
    b
    c
)

fmt.PrintLn(a,b,c) //输出结果10 10 10 

2.5.2 iota枚举

  • iota常量自动生成器,每隔一行,自动加1
  • iota给常量赋值使用
  • iota遇到下个const,会重置为0
  • 多个常量可以写一个iota,在一个括号里
  • 多重赋值,在同一行,值一样

图片

3. 流程控制

  • 3.1 if 条件判断语句
func max(num1, num2 int) int {
   /* 声明局部变量 */
   var result int

   if num1 > num2 {
   	result = num1
   } else {
   	result = num2
   }
   return result
}


  • 3.2 switch 条件选择语句

   grade := ""
   score := 88.5

   switch true {
   case score >=90:
   	grade = "A"
   case score >=80:
   	grade = "B"
   case score >=70:
   	grade = "C"
   default:
   	grade="E"
   }

   fmt.Printf("你的登记是: %s\n",grade )

  • 3.3 for 循环语句
第一种写法:
for i:=0;i<=20 ;i++  {
		fmt.Printf("%d\n",i)
	}
 第二种写法:
 	var i int
	for i<=20  {
		fmt.Printf("%d\n",i)
	}
   第三种写法(for ...range):
   str := "123ABCabc好"
	for i,value := range str{
		fmt.Printf("第 %d 位的ASCII值=%d,字符是%c\n",i,value,value)
	}
   

4.Go语言的函数与指针

4.1 函数

func(参数列表)(返回参数列表){
  //函数体
}

4.1.3 函数变量

函数变量是把函数作为值保存到变量中.

在Golang中,,函数也是一种类型,可以和其他类型一样被保存在变量中

type myFunc func(int) bool
func main(){

   nums:=[]int{10,20,40,16,17,3030,49849,204394,43943,2923,23923,}
   fmt.Println(filter(nums,isEven))
   fmt.Println(filter(nums,isAdd))
}

func filter(arr []int, f myFunc) []int {
   var result []int
   for _, value := range arr {
   	if f(value) {
   		result = append(result, value)
   	}
   }
   return result
}
func isEven(num int) bool{
   if num%2 == 0 {
   	return true
   }else {
   	return false
   }
}
func  isAdd(num int) bool{
   if num%2 == 0 {
   	return false
   }
   return true
}

4.1.4 匿名函数

匿名函数没有函数名,只有函数体,可以作为一种类型赋值给变量。

匿名函数经常被用于实现回调函数、闭包等

1.在定义匿名函数的时候就可以直接使用
res1 := func (n1 int, n2 int) int {
        return n1 + n2
    }(10, 30)  //括号里的10,30 就相当于参数列表,分别对应n1和n2
    
    fmt.Println("res1=",res1)
    
2.将匿名函数赋给一个变量
res1 := func (n1 int, n2 int) int {
		return n1 + n2
	}
	res2 := res1(50,50)
	fmt.Println("res1=",res2)
    
3.匿名函数作为回调函数

func vist(list []float64,f func(float64))  {
	for _,value:=range list{
		f(value)
	}
}
	List := []float64{1,2,5,20,90}
	vist(List, func(v float64) {
		sqrt := math.Pow(v,2)
		fmt.Println(sqrt)
	})
    

4.1.5 闭包

//函数f返回了一个函数,返回的这个函数就是一个闭包。这个函数本身中没有定义变量I的,而是引用了它所在的环境(函数f)中的变量i.
    func f(i int) func() int  {
        return func() int{
            i++
            return i
        }
    }
	a:=f(0)
	fmt.Println(a()) //0
	fmt.Println(a()) //1
	fmt.Println(a()) //2
	fmt.Println(a()) //3


4.1.6 可变参数

语法格式:
func 函数名(参数名...类型)(返回值列表){}

该语法格式定义了一个接受任何数目、任何类型参数的函数。这里特殊语法是三个点"...",在一个变量后面加上三个点,表示从该处开始接受可变参数

func Tsum(nums ...int) {
	fmt.Println(nums)
	total:=0
	for _,val := range nums{
		total+=val
	}
	fmt.Println( total)
}

4.1.7 golang单元测试

要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时需要让文件必须以_test结尾

单元测试源码文件可以由多个测试用例组成,每个测试用例函数需要以Test为前缀,例如:

格式如下:
func TestXXX( t *testing.T )

func sum2(n1 int, args ...int) int {
	sum := n1
	for i := 0; i < len(args); i++ {
		sum += args[i]
	}
	return sum
}
func TestAvaiableSum(t *testing.T) {
	res := sum2(1, 23, 34, 56)
	fmt.Println("res=", res)
}

4.2指针

指针式存储另一个变量的内存地址的变量。变量是一种使用方便的占位符。一个指针变量可以指向任何一个值的内存地址 在Go语言中使用地址符&来获取变量的地址,一个变量前使用&会返回该变量的内存地址

total:=20
fmt.Println("total的内存地址",&total)

4.2.1 声明指针

格式:var 指针变量 *指针类型 声明指针,*T是指针变量的类型,它指向T类型的值,*号用于指定变量是一个指针

var ip *int //指向整型的指针
var fp *float32 //指向浮点型的指针

指针使用流程

1.定义指针变量 2.为指针变量赋值 3.访问指针变量中指向地址的值 获取指针变量指向的变量值:在指针类型的变量前加上号。如a

type Student struct {
	name string
	age int
	sex int8
}

func TestZhiz(t *testing.T)  {
	s1:=Student{"steven",32,2}
	s2:=Student{"Sunny",10,1}
	var a *Student=&s1 //&s1的内存地址
	var b *Student=&s2 //&s2的内存地址
	fmt.Printf("s1类型为%T,值为%v\n",s1,s1)
	fmt.Printf("s2类型为%T,值为%v\n",s2,s2)
	fmt.Printf("a类型为%T,值为%v\n",a,a)
	fmt.Printf("b类型为%T,值为%v\n",b,b)

	fmt.Printf("s1的值等于a指针\n")
	fmt.Printf("s2的值等于b指针\n")
	fmt.Printf("*a类型为%T,值为%v\n",*a,*a)
	fmt.Printf("*b类型为%T,值为%v\n",*b,*b)

}

  • 空指针
if(ptr != nil) //ptr不是空指针
if(ptr == nil)//ptr是空指针

4.2.2 使用指针

1.通过指针修改变量的值

//指针修改变量的值
	a2:=32
	b2:=&a2
	fmt.Println("a2的值",a2) //a2的值 32
	fmt.Println("b2地址",b2) //b2地址 0xc4200142d8
	fmt.Println("b2的值",*b2) //b2的值 32
	*b2++
	fmt.Println("b2的值",*b2) //b2的值 33
    

2.使用指针作为函数的参数

将基本数据类型的指针作为函数的参数,可以实现对传入数据的修改,这是因为指针作为函数的参数只是赋值了一个指针,指针指向的内存没有发生改变

func main(){
orgi:=68
		ptr:=&orgi
		change(ptr)
		fmt.Println("执行函数后orgi的值",orgi) //执行函数后orgi的值 20
}
func change(p *int)  {
	*p=20
}


4.2.3 指针数组

//指针数组
//格式:var ptr [3]*string

ptrArr:=[COUNT]string{"abc","ABC","123","8888"}

i:=0
//定义指针数组
var ptrPoint [COUNT]*string
fmt.Printf("%T,%v \n",ptrPoint,ptrPoint) //[4]*string,[<nil> <nil> <nil> <nil>]

//将数组中的每个元素地址赋值给指针数组
	for i=0;i<COUNT;i++ {
		ptrPoint[i] = &ptrArr[i]
	}

	fmt.Printf("%T,%v \n",ptrPoint,ptrPoint) //[4]*string,[0xc42000e800 0xc42000e810 0xc42000e820 0xc42000e830]

	//循环取指针数组中的值
	for i=0;i<COUNT;i++ {
		fmt.Printf("a[%d]=%v \n",i, *ptrPoint[i])
		//a[0]=abc 
		//a[1]=ABC 
		//a[2]=123 
		//a[3]=8888 
	}

4.2.4 指针的指针

指向指针的指针变量声明格式如下:

var ptr **int//使用两个*号


//指针的指针
var a2 int
var ptr2 *int
var pptr **int
a2=1234
ptr2=&a2
fmt.Println("ptr地址",ptr2)
pptr=&ptr
fmt.Println("pptr地址",pptr)

fmt.Printf("变量a2=%d\n",a2)
fmt.Printf("指针变量ptr2=%d\n",*ptr2)
fmt.Printf("指向指针的指针量pptr=%d\n",**pptr)
//输出结果
/*
ptr地址 0xc4200d4140
pptr地址 0xc4200ec000
变量a2=1234
指针变量ptr2=1234
指向指针的指针量pptr=20
*/

4.3 函数的参数传递

4.3.1 值传递(传值)

值传递是指在调用函数时将实际参数复制一份传递到函数中,不会影响原内容数据

4.3.2 引用传递(传引用)

1.引用传递是在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改将影响原内容数据
2.Go中可以借助指针来实现引用传递。函数参数使用指针参数,传参的时候其实是复制一份指针参数,也就是复制了一份变量地址
3.函数的参数如果是指针,当调用函数时,虽然参数是按复制传递的,但此时仅仅只是复制一个指针,也就是一个内存地址,这样不会造成内存浪费、时间开销

函数传int类型的值与引用对比

package main

import "fmt"

func main()  {

	//函数传int类型的值与引用对比
	a:=200
	fmt.Printf("变量a的内存地址%p,值为:%v\n",&a,a)
	changeIntVal(a)
	fmt.Printf("changeIntVal函数调用后变量a的内存地址%p,值为:%v\n",&a,a)
	changeIntPtr(&a)
	fmt.Printf("changeIntPtr函数调用后变量a的内存地址%p,值为:%v\n",&a,a)
	/*
	变量a的内存地址0xc420080008,值为:200
	changeIntVal函数,传递的参数n的内存地址:0xc420080018,值为:200
	changeIntVal函数调用后变量a的内存地址0xc420080008,值为:200
	changeIntPtr函数,传递的参数n的内存地址:0xc42008a020,值为:0xc420080008
	changeIntPtr函数调用后变量a的内存地址0xc420080008,值为:50
	*/
}
func changeIntVal(n int)  {
	fmt.Printf("changeIntVal函数,传递的参数n的内存地址:%p,值为:%v\n",&n,n)
	n=90
}
func changeIntPtr(n *int)  {
	fmt.Printf("changeIntPtr函数,传递的参数n的内存地址:%p,值为:%v\n",&n,n)
	*n=50
}

函数传slice类型的值与引用对比

import "fmt"
func main()  {
   //函数传slice类型的值与引用对比
   a:=[]int{1,2,3,4}
   fmt.Printf("变量a的内存地址%p,值为:%v\n",&a,a)
   changeSliceVal(a)
   fmt.Printf("changeSliceVal函数调用后变量a的内存地址%p,值为:%v\n",&a,a)
   changeSlicePtr(&a)
   fmt.Printf("changeSlicePtr函数调用后变量a的内存地址%p,值为:%v\n",&a,a)
}
func changeSliceVal(n []int)  {
   fmt.Printf("changeSliceVal函数,传递的参数n的内存地址:%p,值为:%v\n",&n,n)
   n[0]=90
}
func changeSlicePtr(n *[]int)  {
   fmt.Printf("changeSlicePtr函数,传递的参数n的内存地址:%p,值为:%v\n",&n,n)
   (*n)[1]=50
}

5.3 map

  • 5.3.1 map概念

Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。 Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的 Map是hash表的一个引用,类型写为:map[key]value,其中的key, value分别对应一种数据类型,如:map[string]string 要求所有的key的数据类型相同,所有value数据类型相同(注:key与value可以有不同的数据类型)

  • 5.3.2 map语法

声明map

第一种方法
mapVar := map[key类型]value类型
第二种方法
mapVar := make(map[key类型]value类型)

map初始化和遍历

	mapVar:=map[string]string{
		"a":"t1",
		"b":"t2",
		"c":"t3",
	}
	//遍历map
	for key, value := range mapVar {
		fmt.Printf("key=%v value=%v\n",key,value)
	}

	//查看元素在集合中是否存在
	if value,ok:=mapVar["aa"];ok {
		fmt.Println("存在value",value)
	}else {
		fmt.Println("不存在value")

	}


  • 5.3.3 map是引用类型

第6章 Go常用内置包

可参考官网

  • 字符串遍历
str:="strings包:遍历带有中文的字符串"
   for _, value := range []rune(str) {
   	fmt.Printf("%c\n",value)
   }

  • json序列化和反序列化

map序列化

//第1种map声明方式
	M2:= map[int]string{
		2:"aa",
		3:"bb",
	}
	//第2种map声明方式
	M:=make(map[int]string)
	M[1]="aaa"
	M[2]="bbb"
	res2,error:=json.Marshal(M2)
	res,error:=json.Marshal(M)
	if error!=nil {
		fmt.Printf("解析错误")
	}
	fmt.Printf(string(res2))
	fmt.Printf(string(res))
	//返回结果
	//{"2":"aa","3":"bb"}{"1":"aaa","2":"bbb"}

结构体序列化

package main

import (
	"encoding/json"
	"fmt"
)
type Stu struct {
	Name  string `json:"name"`
	Age   int
	HIgh  bool
	sex   string
	Class *Class `json:"class"`
}
type Class struct {
	Name  string
	Grade int
}
func main() {
	//实例化一个数据结构,用于生成json字符串
	cla := new(Class)
	cla.Name = "1班"
	cla.Grade = 3
	stu := Stu{
		"张三",
		18,
		true,
		"男",
		cla,//指针变量
	}
	//Marshal失败时err!=nil
	jsonStu, err := json.Marshal(stu)
	if err != nil {
		fmt.Println("生成json字符串错误")
	}
	//jsonStu是[]byte类型,转化成string类型便于查看
	fmt.Println(string(jsonStu))
     从结果中可以看出
只要是可导出成员(变量首字母大写),都可以转成json。因成员变量sex是不可导出的,故无法转成json。
如果变量打上了json标签,如Name旁边的 json:"name" ,那么转化成的json key就用该标签“name”,否则取变量名作为key,如“Age”,“HIgh”。
bool类型也是可以直接转换为json的value值。Channel, complex 以及函数不能被编码json字符串。当然,循环的数据结构也不行,它会导致marshal陷入死循环。
指针变量,编码时自动转换为它所指向的值,如cla变量。
(当然,不传指针,Stu struct的成员Class如果换成Class struct类型,效果也是一模一样的。只不过指针更快,且能节省内存空间。)



	//反序列化操作Unmarshal()
	var per_data Stu
	err2 := json.Unmarshal([]byte(jsonStu),&per_data)
	if err2 != nil {
		fmt.Printf("反序列化错误:%v\n", err)
	}
	fmt.Printf("personal_json反序列化=%v\n", per_data)
	fmt.Printf("per_data=%v\n", *per_data.Class)
    
}

第7 章Go面向对象

结构体

  • 匿名结构体 和结构体匿名字段

匿名结构体就是没有名字的结构体,无须通过type关键字定义就可以直接使用。创建匿名结构体的时候,同时也要创建结构体对象

	//匿名结构体
	addr:=struct{
		name string
		age int
	}{"slaiven",39}
	fmt.Println(addr)

匿名字段就是在结构体中的字段没有名字,只包含一个没有字段名的类型 如果字段没有名字,那么默认使用类型作为字段名,同一类型只能有一个匿名字段

//匿名字段
	user:=new(User)
	user.string="apce"
	user.int=84
	fmt.Printf("名字%v,年龄%v",user.string,user.int) //名字apce,年龄84

  • 结构体嵌套

将一个结构当作另一结构体的字段(属性),这种就是结构体嵌套,可以模拟以下两种关系. 聚合关系:一个类作为另一个类的属性,一定要采用有名字的结构体作为字段 继承关系:一个类作为另一个类的子类。子类与父类的关系。采用匿名字段的形式,匿名字段就该结构体的父类

//聚合关系:一个类作为另一个类的属性
type Address struct {
	province,city string
}
type Person struct {
	name string
	age int
	address  *Address
}

func TestMoudelStrings(t *testing.T)  {
	//实例化Address结构体
	addr:=Address{}
	addr.province="北京市"
	addr.city="丰台区"
	//实例化Person结构体
	p:=Person{}
	p.name="Strven"
	p.age=28
	p.address=&addr
	fmt.Println("姓名:",p.name,"年龄:",p.age,"省:",p.address.province,"市:",p.address.city)
	//如果修改了Person对象的address数据,那么对Address对象会有影响么?肯定的
	p.address.city="大兴区"
	fmt.Println("姓名:",p.name,"年龄:",p.age,"省:",p.address.province,"市:",addr.city)
	//修改Address对象,是否会影响Persion对象数据?肯定的
	addr.city="朝阳区"
	fmt.Println("姓名:",p.name,"年龄:",p.age,"省:",p.address.province,"市:",addr.city)
}
//继承关系:一个类作为另一个类的子类。子类与父类的关系

type Address struct {
	province,city string
}
type Person struct {
	name string
	age int
	Address //匿名字段,Address是Person的父类
}
func TestMoudelStrings(t *testing.T)  {
	//实例化Address结构体
	addr:=Address{}
	addr.province="北京"
	addr.city="丰台区"
	//实例化Person结构体
	p:=Person{"strven",38,addr}
	fmt.Printf("姓名:%v  年龄:%v 省:%v 市:%v\n",p.name,p.age,p.Address.province,p.Address.city) //姓名:strven  年龄:38 省:北京 市:丰台区
}

方法

  • Go中同时有函数和方法,方法的本质是函数,但是与函数又不同

1.含义不同,函数是一段具有独立功能的代码,可以被反复多次调用,而方法是一个类的行为功能,只有该类的对象才能调用 2.方法有接受者而函数没有,Go语言的方法是一种作用域特定类型变量的函数,这种类型变量叫作接受者(receiver),接受者的概念类似于传统面向对象中的this或self关键字 3.方法可以重名(接受者不同),而函数不能重名,

type Per struct {
	name string
	age int
}
func ( p  Per ) getData()  {
	fmt.Printf("名字:%v 年龄:%v",p.name,p.age) //名字:aaa 年龄:39
}
func TestMethod(t *testing.T)  {
	p1:=Per{"aaa",39}
	p1.getData()
}

  • 方法继承

方法是可以继承的,如果匿名字段实现了一个方法,那么包含这个匿名字段的struct也能调用该匿名字段中的方法

type Human struct {
	name, phone string
	age int
}

type Stu struct {
	Human
	school string
}
type Employee struct {
	Human
	company string
}
func TestMethod(t *testing.T)  {
	s1:=Stu{Human{"dav","1850103930",7}," 洛阳一中"}
	s1.SayHi()
}
func (h *Human) SayHi()  {
	fmt.Printf("我是%s,%d岁,电话%s\n",h.name,h.age,h.phone)
}

  • 方法重写
type Human struct {
	name, phone string
	age int
}

type Stu struct {
	Human
	school string
}
type Employee struct {
	Human
	company string
}
func TestMethod(t *testing.T)  {
	s1:=Stu{Human{"dav","1850103930",7}, " 洛阳一中"}
	s2:=Employee{Human{"dav","1850*****",17},"航天飞机"}
	s1.SayHi()
	
	s2.SayHi()
}
func (h *Human) SayHi()  {
	fmt.Printf("我是%s,%d岁,电话%s\n",h.name,h.age,h.phone)
}
func (h *Stu) SayHi()  {
	fmt.Printf("我是%s,%d岁,电话%s,学校%s\n",h.name,h.age,h.phone,h.school)
}
func (h *Employee) SayHi()  {
	fmt.Printf("我是%s,%d岁,电话%s,工作%s\n",h.name,h.age,h.phone,h.company)
}

第9章 Go文件I/O操作

9.1文件信息

  • FileInfo接口
func main()  {
	file:="./layout.html"
	printMessat(file)
}
func printMessat(filePath string)  {
	fileinfo,error:=os.Stat(filePath)
	if error !=nil {
		fmt.Println("文件打开错误",error.Error())
	}
	fmt.Printf("文件名:%s\n",fileinfo.Name())
	fmt.Printf("文件权限:%s\n",fileinfo.Mode())
	fmt.Printf("是否为目录:%s\n",fileinfo.IsDir())
	fmt.Printf("文件最后修改权限:%s\n",fileinfo.ModTime())
	fmt.Printf("文件大小:%s\n",fileinfo.Size())
}

  • 文件路径
	file1:="/Users/u51/Documents/go_learn/first.go"
	file2:="./layout.html"
	fmt.Printf("是否是绝对路径%v\n",filepath.IsAbs(file1))
	fmt.Printf("是否是绝对路径%v\n",filepath.IsAbs(file2))
	fmt.Printf("获取文件绝对路径%v\n",filepath.Abs(file2))
	fmt.Printf("获取文件绝对路径%v\n",filepath.Abs(file2))

9.2 文件常规操作

  • 创建目录 os.MKdir() 创建一级目录 os.MKdirAll() 创建多级目录
  • 创建文件

os.create() 创建文件

  • 删除文件

os.Remove() 删除文件或空目录 os.RemoveAll() 移除所有所有路径及包含的子节点,

file,error:=os.Create("test1.csv")
if error!=nil{
	fmt.Printf("文件创建失败")
}
file.Write([]byte("aaa\n"))
file.WriteString("bbbb")
file.WriteString("文件")


  • 打开和关闭文件

os.Open从文件开始读取数据,返回值n是实际读取的字节数,如果读到文件末尾n为0或err为io.EOF

file,error:=os.Open("./test1.csv")
if error!=nil{
	fmt.Printf("打开错误")
}else {
	bs:=make([]byte,1024,1024)
	for{
		n,err:=file.Read(bs)
		if n==0||err==io.EOF{
			fmt.Printf("读取文件结束-----")
			break
		}
		fmt.Println(string(bs[:]))
	}
}
defer file.Close()


  • 复制文件
func main() {
	srcFile:="./test.csv"
	destFile:="./test1.csv"
	total,err:=copyFiles(srcFile,destFile)
	if err!=nil{
		fmt.Printf(err.Error())
	}else {
		fmt.Println("复制ok",total)
	}
}
func copyFiles(srcfile,destfile string)(int64,error)  {
	file1,err:=os.Open(srcfile)
	if err!=nil{
		return 0, err
	}
	file2,err:=os.OpenFile(destfile,os.O_RDWR|os.O_CREATE,os.ModePerm)
	if err != nil{
		return 0, err
	}
	defer file1.Close()
	defer file2.Close()
	return  io.Copy(file2,file1)
}

9.3 ioutil包

  • ioutil包核心函数
方法 作用
ReadFile() 读取文件中所有数据,返回读取的字节数组
WriteFile() 向指定文件写入数据,如果文件不存在,则创建文件,写入文件之前清空文件
ReadDir() 读取一个目录下的所有子文件及目录名称
TempDir() 在当前目录下,创建一个以指定字符串为名称前缀的临时文件夹,并返回文件夹路径
TempFile() 在当前目录下,创建一个以指定字符串为名称的前缀的文件,并以读写模式打开,返回os.File指针
	data,error:=ioutil.ReadFile("./test1.csv")
	if error!=nil{
		fmt.Printf("打开错误")
	}else {
		fmt.Println(string(data))
	}

	bs:=[]byte("hello中的空间打开")
	error:=ioutil.WriteFile("./test1.csv",bs,077)
	if error!=nil{
		fmt.Printf("写入文件异常")
	}else {
		fmt.Printf("写入文件成功")
	}

9.4 bufio包

bufio实现了带缓冲的I/O操作,达到高效读写

  • bufio.Reader结构体

bufio.Reader 常用方法

方法 作用
func NewReader(rd io.Reader) *Reader 创建一个具有默认大小缓冲区、从r读取的*Reader
func NewReaderSize(rd io.Reader, size int) *Reader 创建一个具有size大小缓冲区,从r读取的*Reader
func (b *Reader) Discard(n int) (discarded int, err error) 丢弃n个byte数据
func (b *Reader) Read(p []byte) (n int, err error) 读取n个byte数据
func (b *Reader) Buffered() int 返回缓冲区中现有的可读取的字节数
func (b *Reader) Peek(n int) ([]byte, error) 获取当前缓冲区内接下来的n个byte数据,但不是移动指针
func (b *Reader) ReadByte() (byte, error) 读取1个字节
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) 读取一行数据,\n分隔
func (b *Reader) ReadString(delim byte) (string, error) 读取1个字符串
func (b *Reader) ReadRune() (r rune, size int, err error) 读取1个utf-8字符
func (b *Reader) Reset(r io.Reader) 清空缓冲区
  • bufio.Writer结构体

第10章 并行编程

并发与并行

  • 并发:同一时间段内执行多个任务
  • 并行:同一时刻执行多个任务
  • Go语言的并发通过goroutine实现,goroutine类似于线程,属于用户态线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。
  • Go语言还提供channel在多个goroutine间进行通信。goroutinechannel是 Go 语言并发模式的重要实现基础。

goroutine

  • goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU
  • 在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴
  • Go语言中使用goroutine非常简单,只需要在被调用的函数前面加上go 关键字,这样就可以为一个函数创建一个goroutine(也就是创建了一个线程)

一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

启动单个goroutine

package main
func t1Goroutine()  {
	println("第一个goroutine函数")
}
func main() {
	go t1Goroutine()
	println("main over....")
}
//输出结果,为什么没有打印函数t1Goroutine中的内容呢????
main over....


原因: 在程序启动时,Go程序就会为main()函数创建一个默认的goroutine.

当main()函数返回的时候该goroutine就结束了,所有在main()中启动的goroutine会一同结束,main函数所在的goroutine就像是权利的游戏中的夜王,其他的goroutine都是异鬼,夜王一死它转化的那些异鬼也就全部GG了。

所以我们要想办法让main函数等一等hello函数,最简单粗暴的方式就是time.Sleep了。

执行上面代码会发现,先打印main over....然后再打印goroutine的内容 第一个goroutine函数

首先为什么会先打印main over....是因为我们在创建新的goroutine的时候需要花费一些时间,而此时main函数所在的goroutine是继续执行的

启动多个goroutine

package main
import (
	"fmt"
	"sync"
)
//wait group 用来等待一组goroutines的结束,在主Goroutine里声明,并且设置要等待的goroutine的个数,每个goroutine执行完成之后调用 Done,最后在主Goroutines 里Wait即可
var wg sync.WaitGroup //声明一个全局变量
func t2Goroutine(i int )  {
	defer wg.Done()
	fmt.Println("Go ", i)
}
func main() {
	for i:=0;i<10;i++{
		wg.Add(1)
		go t2Goroutine(i)
	}
	wg.Wait()
}
//输出结果
Go  5
Go  1
Go  0
Go  7
Go  8
Go  2
Go  6
Go  9
Go  4
Go  3

多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。

goroutine与线程

可增长的栈

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这么大。所以在Go语言中一次创建十万左右的goroutine也是可以的。

goroutine调度

GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

  • G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
  • P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
  • M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;

P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能

GOMAXPROCS

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码

直接在main中声明runtime.GOMAXPROCS(1)

channel通道

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

声明channel类型

var by1 []int //声明一个int类型的切片
var ch1 chan int //声明一个存放int类型的channel

    • 声明的通道后需要使用make函数初始化之后才能使用。
// 初始化channel
ch2:=make(chan int)
// 初始化带缓冲区的chan
ch3:=make(chan string)

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

  • 现在我们先使用以下语句定义一个通道:
ch := make(chan int)

将一个值发送到通道中

ch<- 10//把数据10发送到ch中

    • 从一个通道中接收值
data:= <-ch// 从ch中接收的值赋值给变量data
<-ch        //从ch中接收值,忽略结果

我们通过调用内置的close函数来关闭通道。

close(ch)

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致panic。

无缓冲的通道

无缓冲通道又称为阻塞的通道

func main()  {
	// 初始化channel
	ch1:=make(chan int)
	ch1 <- 10
	fmt.Printf("发送成功")
}
//输出结果
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
	D:/work/workspace/go_space/goruntine.go:8 +0x5f

为什么会出现deadlock错误呢?

因为我们使用ch1 := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送

上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值,例如:

package main
import "fmt"
func recvData(c chan int)  {
	x:=<-c
	fmt.Println("接收到的数据",x)
}
func main()  {
	// 初始化channel
	ch1:=make(chan int)
	go recvData(ch1)
	ch1 <- 10
	fmt.Printf("发送成功")
}
//输出结果
接收到的数据 10
发送成功

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道

有缓冲的通道

解决上面问题的方法还有一种,使用带有缓冲区的通道

func main()  {
	// 初始化channel
	ch1:=make(chan int ,2)
	ch1 <- 10
	fmt.Printf("发送成功")
}

从通道循环取值

当向通道中发送完数据时,我们可以通过close函数来关闭通道。

package main

import (
	"fmt"
)
func main() {
	ch1:=make(chan int)
	ch2:=make(chan  int)
	// 循环生成100个数,存到ch1中
	go func() {
		for i:=0;i<10;i++{
			ch1<-i
		}
		close(ch1)
	}()
	// 从ch1中接收值,*2后存到ch2
	go func() {
		for{
			data,ok:=<-ch1 //通道关闭后再取值ok=false
			if !ok{
				break
			}
			ch2<-data*data

		}
		close(ch2)
	}()
	//在主goroutine中range ch2
	for i:=range ch2{// 通道关闭后会退出for range循环
		fmt.Println(i)
	}
}
//输出结果
0
1
4
9
16
25
36
49
64
81

从上面的例子中我们看到有两种方式在接收值的时候判断该通道是否被关闭,不过我们通常使用的是for range的方式。使用for range遍历通道,当通道被关闭的时候就会退出for range

单向通道

  • chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;
  • <-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。

第11章 反射

反射可以在程序编译期将变量的信息如字段名称、类型、结构体信息等整合到可执行文件中,这样就可以在程序运行期获取类型的反射信息

Go程序在运行期使用reflect包访问程序的反射信息。

在Go语言的反射机制中,任何接口值都由是一个具体类型具体类型的值两部分组成的。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Typereflect.Value两部分组成,reflect包提供了reflect.TypeOfreflect.ValueOf两个函数来获取任意对象的Value和Type。

reflect.TypeOf()

  • 获取任意值的 类型对象
func reflectType(x interface{}) {
	v := reflect.TypeOf(x)
	fmt.Printf("type:%v\n", v)
}
func main() {
	var a float32 = 3.14
	reflectType(a) // type:float32
	var b int64 = 100
	reflectType(b) // type:int64
}

  • type name和type kind

在反射中关于类型还划分为两种:类型(Type)种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。

Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回

type person struct {
	Name string `json:"name"`
	Age int `json:"age"`

}
func reflectType(x interface{}) {
	v := reflect.TypeOf(x)
	fmt.Printf("type:%v kind:%v\n", v.Name(), v.Kind())
}
func main() {
	var a float32 = 3.14
	reflectType(a) // type:float32
	var b int64 = 100
	reflectType(b) // type:int64
	p:=person{
		"struc",
		20,
	}
	reflectType(p) //type:person kind:struct
    m:=make(map[int]string)
    reflectType(m) //type: kind:map
	var s []string
	reflectType(s) //type: kind:slice
}

  • reflect.ValueOf()reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值的值信息。reflect.Value与原始值之间可以互相转换。
func reValueOf(x interface{})  {
	v:=reflect.ValueOf(x)
	k:=v.Kind()
	switch k {
	case reflect.Int64:
		// v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
		fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
	case reflect.Int32:
		fmt.Printf("type is int32, value is %d\n", int32(v.Int()))
	}
}
func main() {
	var b int64 = 100
	reValueOf(b) // type is int64, value is 100

}

  • 通过反射设置变量值

​ 想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的Elem()方法来获取指针对应的值

func relectSetVal(x interface{})  {
	v:=reflect.ValueOf(x)
    //反射中使用Elem()方法获取指针对应的值
	if v.Elem().Kind()==reflect.Int64{
		v.Elem().SetInt(200)
	}
}
func main() {
	var b int64 = 100
	relectSetVal(&b)
	fmt.Println(b) //200
}

结构体反射

  • 任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()Field()方法获得结构体成员的详细信息。
type person struct {
	Name string `json:"name"`
	Age int `json:"age"`
}
func main() {
	p:=person{"test",20}
	r:=reflect.TypeOf(p)
	fmt.Println(r.NumField(),r.Name(),r.Kind()) //2 person struct
	//判断kind是struct
	if r.Kind()==reflect.Struct{
		// 通过for循环遍历结构体的所有字段信息
		for i:=0;i<r.NumField();i++{
			field := r.Field(i)
			fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
		}
	}
	// 通过字段名获取指定结构体字段信息
	if name_field,ok:=r.FieldByName("Name");ok{
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n", name_field.Name, name_field.Index, name_field.Type, name_field.Tag.Get("json"))
	}
}


第12章 worker pool(goroutine池)

第13章 网络编程

get请求

func main() {
	resp, err := http.Get("http://www.baidu.com/")
	if err != nil {
		fmt.Printf("get failed, err:%v\n", err)
		return
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("read from resp.Body failed, err:%v\n", err)
		return
	}
	fmt.Println(string(body))
}

带参数的get请求

关于GET请求的参数需要使用Go语言内置的net/url这个标准库来处理。

func httpGet2(requestUrl string) (err error) {
	Url, err := url.Parse(requestUrl)
	if err != nil {
		fmt.Printf("requestUrl parse failed, err:[%s]", err.Error())
		return
	}

	params := url.Values{}
	params.Set("username","googlesearch")
	params.Set("passwd","golang")
	Url.RawQuery = params.Encode()
	requestUrl = Url.String()
	fmt.Printf("requestUrl:[%s]\n", requestUrl)
	resp, err := http.Get(requestUrl)
	if err != nil {
		fmt.Printf("get request failed, err:[%s]", err.Error())
		return
	}
	defer resp.Body.Close()
	bodyContent, err := ioutil.ReadAll(resp.Body)
	fmt.Printf("resp status code:[%d]\n", resp.StatusCode)
	fmt.Printf("resp body data:[%s]\n", string(bodyContent))
	return
}

带参数post请求

func httpPost(requestUrl string) (err error) {
	data := url.Values{}
	data.Add("username", "seemmo")
	data.Add("passwd", "da123qwe")

	resp, err := http.PostForm(requestUrl, data)
	if err != nil {
		fmt.Printf("get request failed, err:[%s]", err.Error())
		return
	}
	defer resp.Body.Close()

	bodyContent, err := ioutil.ReadAll(resp.Body)
	fmt.Printf("resp status code:[%d]\n", resp.StatusCode)
	fmt.Printf("resp body data:[%s]\n", string(bodyContent))
	return
}

TCP连接过程

TCP服务端程序的处理流程:
1.监听端口
2.接收客户端请求建立连接
3.创建goroutine处理连接

TCP客户端程序的处理流程:
1.建立与服务端的链接
2.进行数据收发
3.关闭链接

  • 服务端代码
package main
import (
	"bufio"
	"fmt"
	"net"
)

//实现TCP通信步骤:
//1.监听端口
//2.接收客户端请求建立连接
//3.创建goroutine处理连接
func process(conn net.Conn) {
	defer  conn.Close() //延迟关闭
	reader:=bufio.NewReader(conn)
	var b [128]byte
	for {
		n,err:=reader.Read(b[:])
		if err!=nil{
			fmt.Println("read from client failed, err:", err)
			break
		}
		recvStr := string(b[:n])
		fmt.Println("收到client端发来的数据:", recvStr)
		conn.Write([]byte(recvStr)) // 发送数据
	}
}
func main() {
	listen,err:=net.Listen("tcp","127.0.0.1:9090")
	if err!=nil{
		fmt.Println("listen failed, err:", err)
		return
	}
	for{
		conn,err:=listen.Accept()
		if err!=nil {
			fmt.Println("accept failed, err", err)
			continue
		}
		go process(conn)
	}

}

客户端代码

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	conn,err:=net.Dial("tcp","127.0.0.1:9090")

	if err!=nil{
		fmt.Println("err:",err)
		return
	}
	defer conn.Close()
	fmt.Println("请输入数据")
	inputReder:=bufio.NewReader(os.Stdin)
	for{
		input,_:=inputReder.ReadString('\n')
		inputInfo := strings.Trim(input, "\r\n")
		if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
			return
		}
		_, err = conn.Write([]byte(inputInfo)) // 发送数据
		if err != nil {
			return
		}
		buf := [512]byte{}
		n, err := conn.Read(buf[:])
		if err != nil {
			fmt.Println("recv failed, err:", err)
			return
		}
		fmt.Println(string(buf[:n]))
	}
}

第14章 数据库操作

下载依赖

go get -u github.com/go-sql-driver/mysql

创建dbconf包

package dbconf
import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)
// 定义一个全局对象db
var db *sql.DB
func InitDb() (err error)  {
	dsn := "epai:epai@tcp(10.10.10.48:3306)/bind_dns?charset=utf8mb4&parseTime=True"
	// 不会校验账号密码是否正确
	// 注意!!!这里不要使用:=,我们是给全局变量赋值,然后在main函数中使用全局变量db
	db,err = sql.Open("mysql", dsn)
	if err != nil {
		return
	}
	// 尝试与数据库建立连接(校验dsn是否正确)
	err = db.Ping()
	if err != nil {
		return
	}
	return
}
type binddns struct {
	zone string
	data string

}
func QueryRowDemo()  {
	var dns binddns
	sqlStr := "SELECT zone, data from dns_records"
	rows,err:=db.Query(sqlStr)
	defer rows.Close()
	if err !=nil {
		fmt.Printf("query failed, err:%v\n", err)
	}
	for rows.Next() {
		err:=rows.Scan(&dns.zone,&dns.data)
		if err != nil{
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("域名:%s 记录值:%s\n", dns.zone,dns.data)
	}
}
func InserDemo() {
	sqlstr := "INSERT INTO `dns_records` VALUES (9, 'abc.com', '*', 'A', '10.10.10.83', 60, NULL, 'any', 255, 28800, 14400, 86400, 86400, 2015050917, 'ddns.net', 'ns.ddns.net.');"
	res, error := db.Exec(sqlstr)
	if error != nil {
		fmt.Printf("insert failed, err:%v\n", error)
		return
	}
	theID, err := res.LastInsertId() // 新插入数据的id
	if err != nil {
		fmt.Printf("get lastinsert ID failed, err:%v\n", err)
		return
	}
	fmt.Printf("insert success, the id is %d.\n", theID)
}
func UpdateDemo() {
	sqlstr:="update dns_records set data=? where id = ?"
	res,err:=db.Exec(sqlstr,"10.10.10.98",9)
	if err !=nil{
		fmt.Printf("update failed, err:%v\n", err)
		return
	}
	res_row,err:=res.RowsAffected()
	if err !=nil{
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("update success, affected rows:%d\n", res_row)

}


在main.go引入dbconf包

package main

import (
	"fmt"
	"test_go_mod2/dbconf"
)
func main() {

	err:=dbconf.InitDb()
	if err != nil {
		fmt.Printf("init db failed,err:%v\n", err)
		return
	}else {
		fmt.Println("连接mysql成功")
	}
	dbconf.QueryRowDemo()
	dbconf.InserDemo()
	dbconf.UpdateDemo()
}

预处理

为什么预处理?

优化MySQL服务器重复执行SQL的方法,可以提升服务器性能,提前让服务器编译,一次编译多次执行,节省后续编译的成本。

  1. 避免SQL注入问题。

Prepare方法会先将sql语句发送给MySQL服务端,返回一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令。

//预查询
func PreareDemo() {
	sqlStr := "SELECT zone, data from dns_records where id < ?"
	stmt,err:=db.Prepare(sqlStr)
	if err !=nil {
		fmt.Printf("preare query failed, err:%v\n", err)
	}
	rows,err:=stmt.Query(8)
	defer rows.Close()
	if err !=nil {
		fmt.Printf("query failed, err:%v\n", err)
	}
	for rows.Next() {
		err:=rows.Scan(&dns.zone,&dns.data)
		if err != nil{
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("域名:%s 记录值:%s\n", dns.zone,dns.data)
	}
}


15章 gorm操作

作者:yum玩坏了
链接:https://juejin.cn/post/6903353632687652872
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0

评论 (0)

取消