golang基础

本文是Go语言的基础教程,涵盖了从环境配置到实际编程的各个方面。介绍了Go的基本语法,如数据类型、字符串、运算、函数、并发等。特别强调了Go的垃圾回收、内存管理和运行时特性。此外,还详细讲解了Go的包管理和模块系统,以及 Gin 框架的使用,包括路由、中间件、错误处理和文件上传等实战内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

GOlang 基本知识

写在前面

语法、 标准库、第三方库、构件体系和工具链

GOlang 最主要的特性:

自动垃圾回收

更丰富的内置类型

函数多返回值

错误处理

匿名函数和闭包

类型和接口

并发编程

反射

语言交互性

  • 为了简化设计,不支持函数重载和操作符重载
  • 为了避免在 C/C++ 开发中的一些 Bug 和混乱,不支持隐式转换
  • Go 语言通过另一种途径实现面向对象设计(第 10-11 章)来放弃类和类型的继承
  • 尽管在接口的使用方面(第 11 章)可以实现类似变体类型的功能,但本身不支持变体类型
  • 不支持动态加载代码
  • 不支持动态链接库
  • 不支持泛型
  • 通过 recoverpanic 来替代异常机制(第 13.2-3 节)
  • 不支持静态变量

环境变量

  • ** G O R O O T ∗ ∗ 表 示 G o 在 你 的 电 脑 上 的 安 装 位 置 , 它 的 值 一 般 都 是 ‘ GOROOT** 表示 Go 在你的电脑上的安装位置,它的值一般都是 ` GOROOTGoHOME/go`,当然,你也可以安装在别的地方。

  • $GOARCH 表示目标机器的处理器架构,它的值可以是 386、amd64 或 arm。

  • $GOOS 表示目标机器的操作系统,它的值可以是 darwin、freebsd、linux 或 windows。

  • ** G O B I N ∗ ∗ 表 示 编 译 器 和 链 接 器 的 安 装 位 置 , 默 认 是 ‘ GOBIN** 表示编译器和链接器的安装位置,默认是 ` GOBINGOROOT/bin`,如果你使用的是 Go 1.0.3 及以后的版本,一般情况下你可以将它的值设置为空,Go 将会使用前面提到的默认值。

  • 目标机器是指你打算运行你的 Go 应用程序的机器。

    Go 编译器支持交叉编译,也就是说你可以在一台机器上构建运行在具有不同操作系统和处理器架构上运行的应用程序,也就是说编写源代码的机器可以和目标机器有完全不同的特性(操作系统与处理器架构)。

    为了区分本地机器和目标机器,你可以使用 $GOHOSTOS$GOHOSTARCH 设置本地机器的操作系统名称和编译体系结构,这两个变量只有在进行交叉编译的时候才会用到,如果你不进行显示设置,他们的值会和本地机器($GOOS$GOARCH)一样。

    • ** G O P A T H ∗ ∗ 默 认 采 用 和 ‘ GOPATH** 默认采用和 ` GOPATHGOROOT一样的值,但从 Go 1.1 版本开始,你必须修改为其它路径。它可以包含多个 Go 语言源码文件、包文件和可执行文件的路径,而这些路径下又必须分别包含三个规定的目录:srcpkgbin`,这三个目录分别用于存放源码文件、包文件和可执行文件。
    • $GOARM 专门针对基于 arm 架构的处理器,它的值可以是 5 或 6,默认为 6。
    • $GOMAXPROCS 用于设置应用程序可使用的处理器个数与核数

GO运行时:

Go 编译器产生的是本地可执行代码,这些代码仍旧运行在 Go 的 runtime(这部分的代码可以在 runtime 包中找到)当中。这个 runtime 类似 Java 和 .NET 语言所用到的虚拟机,它负责管理包括内存分配、垃圾回收、栈处理、goroutine、channel、切片(slice)、map 和反射(reflection)等等。

runtime 主要由 C 语言编写(Go 1.5 开始自举),并且是每个 Go 包的最顶级包。你可以在目录 $GOROOT/src/runtime 中找到相关内容。

垃圾回收器 Go 拥有简单却高效的标记-清除回收器。它的主要思想来源于 IBM 的可复用垃圾回收器,旨在打造一个高效、低延迟的并发回收器。目前 gccgo 还没有回收器,同时适用 gc 和 gccgo 的新回收器正在研发中。使用一门具有垃圾回收功能的编程语言不代表你可以避免内存分配所带来的问题,分配和回收内容都是消耗 CPU 资源的一种行为。

Go 的可执行文件都比相对应的源代码文件要大很多,这恰恰说明了 Go 的 runtime 嵌入到了每一个可执行文件当中。当然,在部署到数量巨大的集群时,较大的文件体积也是比较头疼的问题。但总的来说,Go 的部署工作还是要比 Java 和 Python 轻松得多。因为 Go 不需要依赖任何其它文件,它只需要一个单独的静态文件,这样你也不会像使用其它语言一样在各种不同版本的依赖文件之间混淆。

GO解释器:可以实现REPL(read-eval-print loop)

Sebastien Binet 已经使用这种环境实现了一个 Go 解释器,你可以在这个页面找到:https://github.com/sbinet/igo

从 Go 1 版本开始,使用 Go 自带的更加方便的工具来构建应用程序:

  • go build 编译自身包和依赖包
  • go install 编译并安装自身包和依赖包

链接




格式化代码fmt

tab表示4个空格

workspace

用于放置一个go程序员的所有go代码和依赖在一个workspace里。

前提是生成出的二进制文件不重名,如果存在重名情况,就要分成不同的workspace

workspace:包含多个版本控制的repository

    repository:包含多个package

        package:包含多个go源码文件

src:go源码文件

pkg:package object(编译出的二进制文件)

bin:可执行文件(编译出的二进制文件)

基础语法

细节

//golang中一行代表一个语句的阶数,每个语句不是按分号结尾
// ebnf范式
//工具cgo提供了对FFI(外部函数接口)的支持,使得GO代码能够安全地调用C语言库

//与c交互
import "c"//== #include<cstdio> <stdlib>
import "unsafe"

var i int
C.uint(i) //从go中的int转换成c中无符号Int
int(C.random()) //从C中random()函数返回的long转换为GO中的int

//Random()和Seed()分别调用C中的C.random()和C.srandom()


go的基本要素

package main  //包,package main表示一个可独立运行的程序,必须在源文件的非注释第一行,指明这个文件属于哪个包
//如果对一个包进行更改或者重新编译,那所有引用这个包的客户端程序都必须全部重新编译,包之间通过import关键字将一组包联系在一起
import "fmt"
import "os"
//或者 import "fmt";import "os"
func main(){
    fmt.Println("hello go")
}

数据类型

布尔型: var b bool=true

数字类型:int float32 float64 ,支持整型(32位浮点,64位浮点)和浮点型(complex64 complex128),其中位运算采用补码

字符串类型: GO中的字符串和字节使用UTF-8编码标识的Unicode文本

派生类型:指针Pointer 数组 结构化类型(struct) Channel类型 函数类型 切片类型 接口类型(interface) Map类型

其他类型:byte(类似无符号8位,就是0到255) rune(~= int 32) uintptr(无符号整形,用于存放一个指针)

//1.指定类型,如果没有初始化,则变量默认值为零值(系统默认的值)
如果不赋初值,会有默认值:Bool型为false  字符串为""(空串)
以下几种类型为nil:
var a *int 
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error//erron 是接口

//2.根据值自行判断变量的类型
var v_name=val
package main
import "fmt"
func main(){
    var d=true  //赋过来的值是true 所以d是bool
    fmt.Println(d)
}

//3.第三种,省略 var, 注意 := 左侧如果没有声明新的变量,就产生编译错误
//注意这个:=只能给一个变量用,不能出现a,b:=5,6的情况
v_name:=value
var intVal int
intVal:=1 //这是错误的,因为intVal已经声明,不需要重新声明
intVal :=1//不会产生编译错误,因为:=的左边是有声明新的变量

func main(){
    var a string="abc"
    fmt.Println("hello world")
}//到这里会出错: a declared and not used
所以应该改成   fmt.Println("hello world",a)
//但是全局变量是允许声明但是不使用的

//空白标识符在函数返回值时使用
func main(){
    _,numb,strs:=numbers()//这个下划线表示空白标识符,这样只会获取函数返回值的后面两个
    fmt.Println(numb,strs)
}
func numbers()(int,int,string){
    a,b,c:=1,1,"str"
    return a,b,c
}






//常量
//常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

const identifier [type] =value
//可以省略类型说明符[type],编译器可以根据变量的值来推断其类型
//显示类型: const b string="abc"
//隐式类型: const b="abc"
//常量不能够:=
//常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过,len()返回字符串有多少个字符,不包含空串,cap()
//有2个很重要的内置函数:len()和cap()

len()可以用来查看数组或slice的长度

cap()可以用来查看数组或slice的容量计算容量的方法
cap()可以测量切片最长可以达到多少

在数组中由于长度固定不可变,因此len(arr)cap(arr)的输出永远相同

在slice中,len(sli)表示可见元素有几个(也即直接打印元素看到的元素个数),而cap(sli)表示所有元素有几个,比如:

arr := []int{2, 3, 5, 7, 11, 13}
sli := arr[1:4]
//将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片
fmt.Println(sli)
fmt.Println(len(sli))
fmt.Println(cap(sli))

输出
[3 5 7]//说明是左开右闭
3
5

//iota,特殊常量,可以认为是一个可以被编译器修改的常量。

iotaconst关键字出现时将被重置为 0(const 内部的第一行之前)const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)iota 可以被用作枚举值:
const (
    a = iota
    b = iota
    c = iota
)

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:
const (
    a = iota
    b
    c
)


//左移n位表示乘以2的n次方
<<n==*(2^n)

const (
    i=1<<iota
    j=3<<iota
    k
    l
)

func main() {
    fmt.Println("i=",i)
    fmt.Println("j=",j)
    fmt.Println("k=",k)
    fmt.Println("l=",l)
}
//输出
i= 1
j= 6   //110
k= 12  //1100
l= 24  //11000








reflect.TypeOf().Kind()
//这个函数可以知道某个变量的类型,字符串是以byte数组形式保存的,类型是uint8,占1个byte,打印时需要用string进行类型转换,否则打印的是编码值
//因为字符串是以 byte 数组的形式存储的,所以,str2[2] 的值并不等于语。str2 的长度 len(str2) 也不是 4,而是 8( Go 占 2 byte,语言占 6 byte),也就是说字符串的长度是8的倍数
str2:="go语言"
fmt.Println(str1[2],string(str1[2]))

字符串

package main

import (
	"fmt"
	"reflect"
)
func main() {
    str1 := "Golang"
    str2 := "Go语言"
    fmt.Println(reflect.TypeOf(str2[2]).Kind()) // uint8
    fmt.Println(str1[2], string(str1[2]))       // 108 l
    fmt.Printf("%d %c\n", str2[2], str2[2])     // 232 è
    fmt.Println("len(str2):", len(str2))       //len(str2): 8
}
/*reflect.TypeOf().Kind() 可以知道某个变量的类型,我们可以看到,字符串是以 byte 数组形式保存的,类型是 uint8,占1个 byte,打印时需要用 string 进行类型转换,否则打印的是编码值。
因为字符串是以 byte 数组的形式存储的,所以,str2[2] 的值并不等于语。str2 的长度 len(str2) 也不是 4,而是 8( Go 占 2 byte,语言占 6 byte) */

正确的处理方式是将 string 转为 rune 数组
str2 := "Go语言"
runeArr := []rune(str2)
fmt.Println(reflect.TypeOf(runeArr[2]).Kind()) // int32
fmt.Println(runeArr[2], string(runeArr[2]))    // 35821 语
fmt.Println("len(runeArr):", len(runeArr))    // len(runeArr): 4
转换成 []rune 类型后,字符串中的每个字符,无论占多少个字节都用 int32 来表示,因而可以正确处理中文。



运算

&	按位与运算符"&"是双目运算符。 其功能是参与运算的两数各对应的二进位相与。	(A & B) 结果为 12, 二进制为 0000 1100


|	按位或运算符"|"是双目运算符。 其功能是参与运算的两数各对应的二进位相或	(A | B) 结果为 61, 二进制为 0011 1101


^	按位异或运算符"^"是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1(A ^ B) 结果为 49, 二进制为 0011 0001
//异或就是:0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0(同为0,异为1)


<<	左移运算符"<<"是双目运算符。左移n位就是乘以2的n次方。 其功能把"<<"左边的运算数的各二进位全部左移若干位,由"<<"右边的数指定移动的位数,高位丢弃,低位补0。	A << 2 结果为 240 ,二进制为 1111 0000


>>	右移运算符">>"是双目运算符。右移n位就是除以2的n次方。 其功能是把">>"左边的运算数的各二进位全部右移若干位,">>"右边的数指定移动的位数。	A >> 2 结果为 15 ,二进制为 0000 1111


//运算符优先级
5	* / % << >> & &^
4	+ - | ^	  
3	== != < <= > >=
2   &&
1   ||


函数

只有当某个函数需要被外部包调用的时候才使用大写字母开头,并遵循 Pascal 命名法;否则就遵循骆驼命名法,即第一个单词的首字母小写,其余单词的首字母大写。

这些函数只可以用于调试阶段,在部署程序的时候务必将它们替换成 fmt 中的相关函数。

当被调用函数的代码执行到结束符 } 或返回语句时就会返回,然后程序继续执行调用该函数之后的代码。

程序正常退出的代码为 0 即 Program exited with code 0;如果程序因为异常而被终止,则会返回非零值,如:1。这个数值可以用来测试是否成功执行一个程序。

func function_name([parameter list])[return_types]{
    函数体
}
func:函数的声明
function_name:函数名称,参数列表和返回值构成函数签名
parameter list:参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
return_type:返回类型,函数返回一列值。

//返回两个数的最大值
func max(num1,num2 int)int{
    var result int
    if(num1>num2){
        result=num1
    }else{
        result=num2
    }
    return result
}
func main(){
    a:=5
    b:=6
    c=max(a,b)
    fmt.Println(c)
}

关键字 标识符

//程序一般由关键字,常量,变量,运算符,函数组成
//分隔符:() [] {}
//标识符:一个标识符实际上就是一个或是多个字母(A~Z和a~z)数字(0~9)、下划线_组成的序列,但是第一个字符必须是字母或下划线而不能是数字。
//标点符号: ,  .  ;  :  ...
//空格:变量的声明必须使用空格隔开 eg: var age int;

//25个关键字或者保留字
break	default	func	interface	select
case	defer	go	map	struct
chan	else	goto	package	switch
const	fallthrough	if	range	type
continue	for	import	return	var

//36个预定义标识符
append	bool	byte	cap	close	complex	complex64	complex128	uint16
copy	false	float32	float64	imag	int	int8	int16	uint32
int32	int64	iota	len	make	new	nil	panic	uint64
print	println	real	recover	string	true	uint	uint8	uintptr
package main

import (
    "fmt"  //标准格式,用于规范
    "os"
)

type point struct {//定义结构体
    x, y int  // xy 都是int型
}

func main() {

    p := point{1, 2}  
    fmt.Printf("%v\n", p)

    fmt.Printf("%+v\n", p)

    fmt.Printf("%#v\n", p)

    fmt.Printf("%T\n", p)

    fmt.Printf("%t\n", true)

    fmt.Printf("%d\n", 123)

    fmt.Printf("%b\n", 14)

    fmt.Printf("%c\n", 33)

    fmt.Printf("%x\n", 456)

    fmt.Printf("%f\n", 78.9)

    fmt.Printf("%e\n", 123400000.0)
    fmt.Printf("%E\n", 123400000.0)

    fmt.Printf("%s\n", "\"string\"")

    fmt.Printf("%q\n", "\"string\"")

    fmt.Printf("%x\n", "hex this")

    fmt.Printf("%p\n", &p)

    fmt.Printf("|%6d|%6d|\n", 12, 345)

    fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)

    fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)

    fmt.Printf("|%6s|%6s|\n", "foo", "b")

    fmt.Printf("|%-6s|%-6s|\n", "foo", "b")

    s := fmt.Sprintf("a %s", "string")
    fmt.Println(s)
    fmt.Fprintf(os.Stderr, "an %s\n", "error")
}

条件语句

select语句,类似于switch,但是它会随机执行一个可运行的case,如果没有case可运行,它会阻塞,知道有case可以运行

switch 语句//和其他语言不同的地方在于,Go 语言的 switch 不需要 break,匹配到某个 case,执行完该 case 定义的行为后,默认不会继续往下执行。如果需要继续往下执行,需要使用 fallthrough
type Gender int8
const(   //go中没有enum概念,一般可以用const方式来模拟枚举
	MALE Gender=1
	FEMALE Gender=2	
)
type Gender int8

const (
	MALE Gender =1
	FEMALE Gender=2
)

func main(){
	gender:=MALE
	switch gender{
	case MALE:
		fmt.Println("male")
	case FEMALE:
		fmt.Println("famale")
	default:
		fmt.Println("unknown")
	}
}
//male
//这个default在有case的情况下是不会默认执行的


const (
	MALE Gender =1
	FEMALE Gender=2
)

func main(){
	gender:=MALE
	switch gender{
	case MALE:
		fmt.Println("male")
		fallthrough
	case FEMALE:
		fmt.Println("famale")
		fallthrough
	default:
		fmt.Println("unknown")
	}
}
//male famale unknown

循环语句

goto语句:将控制转移到被标记的语句

函数参数

值传递

func swap(x,y int)int{
    var temp int
    temp=x
    x=y
    y=temp
    return temp
}//值传递
func main(){
   var a int=5
   var b int=6
    swap(a,b)
    fmt.Println(a,b)
}
//5,6

引用传递

func swap(x *int,y *int){
	var temp int
	temp=*x
	*x=*y
	*y=temp
}

func main() {
var a int=100
var b int=200
swap(&a,&b)//传进来的指针要取引用
    

fmt.Println(a,b)
}



函数作为实参

//Go 语言可以很灵活的创建函数,并作为另外一个函数的实参。以下实例中我们在定义的函数中初始化一个变量,该函数仅仅是为了使用内置函数 math.sqrt()

import (
	"fmt"
	"math"
)


func main() {
getSquareRoot:= func(x float64)float64 {
	return math.Sqrt(x)
}
fmt.Println(getSquareRoot(9))
}


//函数调用回调
type cb func(int) int

func main() {
testCallBack(1, callBack)

testCallBack(2, func(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
})
}

func testCallBack(x int, f cb) {
f(x)
}

func callBack(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
}

我是回调,x:1
我是回调,x:2

函数变量作用域

Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑

初始化局部和全局变量:

int 0

float32 0

pointer nil

闭包

//Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

func getSequence()func()int{
	i:=0
	return func() int {
		i+=1
		return i
	}
}//我们创建了函数 getSequence() ,返回另外一个函数。该函数的目的是在闭包中递增 i 变量

func main() {
	nextNumber:=getSequence()/* nextNumber 为一个函数,函数 i 为 0 */
	/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
	fmt.Println(nextNumber())
	fmt.Println(nextNumber())
	fmt.Println(nextNumber())
	/* 创建新的函数 nextNumber1,并查看结果 */
	nextNumber1:=getSequence()
	fmt.Println(nextNumber1())
	fmt.Println(nextNumber1())
}
//输出
1
2
3
1
2

方法


//Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。

func (variable_name variable_type_name)function_name(para_name para_type) return_type{
    //函数体
} 

//下面定义一个结构体类型和该类型的一个方法
type Circle struct {
	redius float64
}

func main() {
	var c1 Circle
	c1.redius=10.00
	fmt.Println("圆的面积",c1.getArea())
}
//该method属于Circle类型对象中的方法
func (c Circle)getArea()float64{
	//c.redius是Circle类型对象中的属性
	return 3.14*c.redius*c.redius
}



结构体

//结构体类似于其他语言中的class 可以在结构体中定义多个字段,为结构体实现方法,实例化等。接下来我们定义一个结构体 Student,并为 Student 添加 name,age 字段,并实现 hello() 方法


type Student struct{
    name string
    age int
}

func(stu *Student)hello(person string)string{
    return fmt.Sprintf("hello %s, i am %s",person,stu.name)
}

func main(){
    stu:=&Student{
        name:"Tom",//这里如果没有这个逗号会报错,但是如果在逗号后面加_也会报错
    }
    msg:=stu.hallo("Jack")
    fmt.Println(msg)
}
//hello Jack, i am Tom

//	使用 Student{field: value, ...}的形式创建 Student 的实例,字段不需要每个都赋值,没有显性赋值的变量将被赋予默认值,例如 age 将被赋予默认值 0。
//	实现方法与实现函数的区别在于,func 和函数名hello 之间,加上该方法对应的实例名 stu 及其类型 *Student,可以通过实例名访问该实例的字段name和其他方法了。
//	调用方法通过  实例名.方法名(参数) 的方式。

func main() {
	stu2 := new(Student)
	fmt.Println(stu2.hello("Alice")) 
    // hello Alice, I am  , name 被赋予默认值""
}

golang的OOP

//C++ 等语言中,实现类的方法做法都是编译器隐式的给函数加一个 this 指针,而在 Go 里,这个 this 指针需要明确的申明出来,其实和其它 OO 语言并没有很大的区别。

class Circle{
public:
    float getArea(Circle& c){
        return 3.14*radius*radius;
    }
private:
    float radius
}

//golang
func(c Circle) getArea()float64{
    //c.radius即为Circle类型对象中的属性
    return 3.14*radius*radius
}



//关于值和指针,如果想在方法中改变结构体类型的属性,需要对方法传递指针,体会如下对结构体类型改变的方法 changRadis() 和普通的函数 change() 中的指针操作:
//同样应该优先传引用
package main

import (
   "fmt"  
)

/* 定义结构体 */
type Circle struct {
  radius float64
}


func main()  { 
   var c Circle
   fmt.Println(c.radius)
   c.radius = 10.00
   fmt.Println(c.getArea())
    c.changeRadius(20)//为什么不是*c.change()
   fmt.Println(c.radius)
   change(&c, 30)
   fmt.Println(c.radius)
}
func (c Circle) getArea() float64  {
   return c.radius * c.radius
}
// 注意如果想要更改成功c的值,这里需要传指针
func (c *Circle) changeRadius(radius float64)  {
   c.radius = radius
}

// 以下操作将不生效
//func (c Circle) changeRadius(radius float64)  {
//   c.radius = radius
//}
// 引用类型要想改变值需要传指针
func change(c *Circle, radius float64)  {
   c.radius = radius
}


数组

var variable_name[SIZE]variable_type

//长度为10的类型为float32
var balance[10]float32

//初始化数组
var balance=[5]float32{1000.0,2.0,3.0,4.0,5.0}

//快速初始化
balance:=[5]float32{1.0,2.0,3.0,4.0,5.0}

//数组长度不定,可用...代替数组长度编译器会自动推断数组长度
var balance[...]float32{1.0}
balance:=[...]float32{1.0}

//如果初始化了数组长度,可用通过指定下标来初始化元素
balance:=[5]float32{1:2.0,3:7.0}

//初始化数组中 {} 中的元素个数不能大于 [] 中的数字。

//如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:
balance[4]=50//设置了第5个元素为50

//访问数组元素数组元素可以通过索引(位置)来读取。格式为数组名后加中括号,中括号中为索引的值
var salary float32=balance[9]
func main()  {
	var n[10]int
	var i,j int
	for i=0;i<10;i++{
		n[i]=i+100
	}

	for j=0;j<10;j++{
		fmt.Printf("elelment[%d]=%d\n",j,n[j])//注意这个printf
	}
}




二维数组

var arrayName[x][y]variable_type


variable_type是GO的数据类型,x是行,y是列
二维数组可以用a[i][j]访问

package main()
import "fmt"
func main()  {
	values:=[][]int{}//创建数组
	//使用append()函数向空的二维数组添加两行一维数组
	row1:=[]int{1,2,3}
	row2:=[]int{4,5,6}
    values=append(values,row1)//把row1添加到values中
	values=append(values,row2)

	//显示两行数据
	fmt.Println("row 1")
	fmt.Println(values[0])
	fmt.Println("row2")
	fmt.Println(values[1])
	fmt.Println("第一个元素")
	fmt.Println(values[0][0])
}








切片

//切片包含三个组件:容量,长度和指向底层数组的指针,切片可以随时进行扩展
//声明一个未指定大小的数组来定义切片
var identifier []type
//切片不需要说明长度
//make()来创建切片,len是数组的长度,也是切片的初始长度
var slice1[]type=make([]type,len)
slice1:=make([]type,len)
//也可以指定容量,其中capacity为可选参数
make([]T,length,capacity)


//切片初始化,[]表示切片类型,cap=len=3  cap是最大可切割长度
s:=[]int{1,2,3}
//初始化切片s,是数组arr的引用
s:=arr[:]
//将arr从下标startIndex到endIndex-1做一个新的切片
s:=arr[startIndex:endIndex]
s:=arr[startIndex:]//从startIndex切到最后一个元素
s:=arr[:endIndex]//从初始切到endIndex
//通过切片S初始化s1
s:=s1[startIndex:endIndex]
//通过make()初始化s
s:=make([]int,len,cap)

//len(),cap()
切片是可以索引的,并且可以通过len()方法来获取长度
切片提供计算容量的方法cap()可以测量切片最长可以达到多少
len()<=cap()

package main
import"fmt"
func main(){
    var numbers=make([]int,3,5)
    printSlice(numbers)
}
func printSlice(x int[]){
    fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
//输出 len=3 cap=5 Slice=[0 0 0]


//空切片
一个切片在未初始化之前默认为nil,长度为0
var numbers[]int//这时候numbers=nil





append() copy()

深入理解slice len cap什么算法? 参数传递有啥蹊跷?

1、内置append函数在现有数组的长度 < 1024 时 cap 增长是翻倍的,再往上的增长率则是 1.25,至于为何后面会说。
2、Go语言中channel,slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)
3、在函数内用append时,append会自动以倍增的方式扩展slice_2的容量,但是扩展也仅仅是函数内slice_2的长度和容量,slice_1的长度和容量是没变的,所以在函数外打印时看起来就是没变。
4、当程序要求slice的容量超大并且需要频繁的更改slice的内容时,就不应该用slice,改用list更合适。
5、Go 在 append 和 copy 方面的开销是可预知+可控的,应用上简单的调优有很好的效果。这个世界上没有免费的动态增长内存,各种实现方案都有设计权衡。

函数外的slice叫slice_1,函数的参数叫slice_2,当函数传递slice_1的时候,其实传入的确实是slice_1参数的复制,所以slice_2复制了slise_1,但要注意的是slice_2里存储的数组的指针,所以当在函数内更改数组内容时,函数外的slice_1的内容也改变了。在函数内用append时,append会自动以倍增的方式扩展slice_2的容量,但是扩展也仅仅是函数内slice_2的长度和容量,slice_1的长度和容量是没变的,所以在函数外打印时看起来就是没变。

append的运作机制
在对slice进行append等操作时,可能会造成slice的自动扩容。其扩容时的大小增长规则是:

如果新的slice大小是当前大小2倍以上,则大小增长为新大小

否则循环以下操作:如果当前slice大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长。直到增长的大小超过或等于新大小。

append的实现只是简单的在内存中将旧slice复制给新slice

//append就是附加的意思,是在后面添加
newcap := old.cap
if newcap+newcap < cap {
    newcap = cap
} else {
    for {
        if old.len < 1024 {
            newcap += newcap
        } else {
            newcap += newcap / 4
        }
        if newcap >= cap {
            break
        }
    }
}

为何不用动态链表实现slice?
首先拷贝一断连续的内存是很快的,假如不想发生拷贝,也就是用动态链表,那你就没有连续内存。此时随机访问开销会是:链表 O(N), 2倍增长块链 O(LogN),二级表一个常数很大的O(1)。问题不仅是算法上开销,还有内存位置分散而对缓存高度不友好,这些问题i在连续内存方案里都是不存在的。除非你的应用是狂append然后只顺序读一次,否则优化写而牺牲读都完全不 make sense. 而就算你的应用是严格顺序读,缓存命中率也通常会让你的综合效率比拷贝换连续内存低。

对小 slice 来说,连续 append 的开销更多的不是在 memmove, 而是在分配一块新空间的 memory allocator 和之后的 gc 压力(这方面对链表更是不利)。所以,当你能大致知道所需的最大空间(在大部分时候都是的)时,在make的时候预留相应的 cap 就好。如果所需的最大空间很大而每次使用的空间量分布不确定,那你就要在浪费内存和耗 CPU 在 allocator + gc 上做权衡。

Go 在 append 和 copy 方面的开销是可预知+可控的,应用上简单的调优有很好的效果。这个世界上没有免费的动态增长内存,各种实现方案都有设计权衡。

什么时候该用slice?
在go语言中slice是很灵活的,大部分情况都能表现的很好,但也有特殊情况。
当程序要求slice的容量超大并且需要频繁的更改slice的内容时,就不应该用slice,改用list更合适。

//如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来
//从拷贝切片的 copy 方法和向切片追加新元素的 append 方法

func main() {
	var numbers []int
	printSlice(numbers)
	//允许追加空切片
	numbers=append(numbers,0)
	printSlice(numbers)
	//向切片添加一个元素
	numbers=append(numbers,1)
	printSlice(numbers)
	//同时添加多个元素
	numbers=append(numbers,2,3,4)
	printSlice(numbers)
	//创建Numbers1是之前切片的两倍容量
	numbers1:=make([]int,len(numbers),(cap(numbers)*2))
	//拷贝Numbers中的内容到numbers1
	copy(numbers1,numbers)
	printSlice(numbers1)
}

func printSlice(x []int)  {
	fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

//输出
len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]


范围(range)

range关键字用于for循环中迭代数组array,切片slice,通道channel,集合(map)的元素。在数组和切片中,它返回元素的索引和索引对应的值

func main() {
	//用range去求一个slice的和。使用数组跟这个很类似
	nums:=[]int{2,3,4}
	sum:=0
	for _,num:=range nums{
		sum+=num
	}
	fmt.Println("sum",sum)

	for i,num:=range nums{
		if num==3{
			fmt.Println("index",i)
		}
	}
	//range也可以用在map的键值对上
	kvs:=map[string]string{"a":"apple","b":"banana"}
	for k,v:=range kvs{
		fmt.Printf("%s->%s\n",k,v)
	}
	//range也可以用来枚举unicode字符串
	for i,c:=range "go"{
		fmt.Println(i,c)
	}

}

MAP(集合)

//定义map

//声明变量,默认map是nil
var map_variable map[key_data_type]value_data_type
//使用make函数,如果不初始化Map,就会创建一个Nil map,nil map是不能存放键值对
map_variable:=make(map[key_data_type]value_data_type)


//应用
func main() {
	var countyCaptialMap map[string]string //创建集合
	countyCaptialMap=make(map[string]string)
	//map插入键值对
	countyCaptialMap["France"]="巴黎"
	countyCaptialMap["Japan"]="东京"
	//使用键输出地图值
	for country:=range countyCaptialMap{
		fmt.Println(country,"首都是",countyCaptialMap[country])
	}
//查看集合中元素是否存在
capital,ok:=countyCaptialMap["American"]
fmt.Println(capital)//nil空键值?
fmt.Println(ok)//false
if(ok){fmt.Println("american首都是:",capital)
}else{
	fmt.Println("不存在")
}
}
//输出
France 首都是 巴黎
Japan 首都是 东京

false
不存在



//map中的delete函数
   /* 创建map */
        countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}

        fmt.Println("原始地图")

        /* 打印地图 */
        for country := range countryCapitalMap {
                fmt.Println(country, "首都是", countryCapitalMap [ country ])
        }

        /*删除元素*/ delete(countryCapitalMap, "France")
        fmt.Println("法国条目被删除")

        fmt.Println("删除元素后地图")

        /*打印地图*/
        for country := range countryCapitalMap {
                fmt.Println(country, "首都是", countryCapitalMap [ country ])
        }

//输出
原始地图
France 首都是 Paris
Italy 首都是 Rome
Japan 首都是 Tokyo
India 首都是 New delhi
法国条目被删除
删除元素后地图
Italy 首都是 Rome
Japan 首都是 Tokyo
India 首都是 New delhi

golang实现简单的hashmap

package main

import (
    "fmt"
)

type HashMap struct {
    key string
    value string
    hashCode int
    next *HashMap
}

var table [16](*HashMap)

func initTable() {
    for i := range table{
        table[i] = &HashMap{"","",i,nil}
    }
}

func getInstance() [16](*HashMap){
    if(table[0] == nil){
        initTable()
    }
    return table
}

func genHashCode(k string) int{
    if len(k) == 0{
        return 0
    }
    var hashCode int = 0
    var lastIndex int = len(k) - 1
    for i := range k {
        if i == lastIndex {
            hashCode += int(k[i])
            break
        }
        hashCode += (hashCode + int(k[i]))*31
    }
    return hashCode
}

func indexTable(hashCode int) int{
    return hashCode%16
}

func indexNode(hashCode int) int {
    return hashCode>>4
}

func put(k string, v string) string {
    var hashCode = genHashCode(k)
    var thisNode = HashMap{k,v,hashCode,nil}

    var tableIndex = indexTable(hashCode)
    var nodeIndex = indexNode(hashCode)

    var headPtr [16](*HashMap) = getInstance()
    var headNode = headPtr[tableIndex]

    if (*headNode).key == "" {
        *headNode = thisNode
        return ""
    }

    var lastNode *HashMap = headNode
    var nextNode *HashMap = (*headNode).next

    for nextNode != nil && (indexNode((*nextNode).hashCode) < nodeIndex){
        lastNode = nextNode
        nextNode = (*nextNode).next
    }
    if (*lastNode).hashCode == thisNode.hashCode {
        var oldValue string = lastNode.value
        lastNode.value = thisNode.value
        return oldValue
    }
    if lastNode.hashCode < thisNode.hashCode {
        lastNode.next = &thisNode
    }
    if nextNode != nil {
        thisNode.next = nextNode
    }
    return ""
}

func get(k string) string {
    var hashCode = genHashCode(k)
    var tableIndex = indexTable(hashCode)

    var headPtr [16](*HashMap) = getInstance()
    var node *HashMap = headPtr[tableIndex]

    if (*node).key == k{
        return (*node).value
    }

    for (*node).next != nil {
        if k == (*node).key {
            return (*node).value
        }
        node = (*node).next
    }
    return ""
}

//examples 
func main() {
    getInstance()
    put("a","a_put")
    put("b","b_put")
    fmt.Println(get("a"))
    fmt.Println(get("b"))
    put("p","p_put")
    fmt.Println(get("p"))
}

指针

str:="golang"
var p *string=&str //p是指向str的指针
*p="Hello"
fmt.Println(str)

golang的递归函数

//Go 语言支持递归。但我们在使用递归时,开发者需要设置退出条件,否则递归将陷入无限循环中。

//递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成斐波那契数列等。
func recursion(){
    recursion()
}
func main(){
    recursion()
}
//阶乘
func Factorial(n uint64) (result uint64) {
	if(n>0){
		result=n*Factorial(n-1)
		return result
	}
	return 1//n<=0时,函数值都为1
}
func main() {
	var i int=15
	fmt.Printf("%d 的阶乘是 %d",i,Factorial(uint64(i)))
}
//结果
15 的阶乘是 1307674368000



//斐波拉契数列
func fibonacii(n uint) uint {
	if n<2 {
		return n
	}
	return fibonacii(n-2)+fibonacii(n-1)
}
func main() {
	var i uint
	for i=0;i<10;i++{
		fmt.Printf("%d   ",fibonacii(i))
	}
}

golang的类型转换

type(a)//type是类型,a是表达式
//将整型转化为浮点型,并计算结果,将结果赋值给浮点型变量
func main(){
    var sum int=17
    var count int=5
    var mean float32
    mean=float32(sum)/float32(count)
    fmt.Printf("mean 的值是:%f\n",mean)
}
//输出
3.400000

接口

//golang貌似没有模板,只有接口
//Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
type interface_name interface{
    method_name1 [return_type]
    method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
   /* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
   /* 方法实现*/
}


//应用
type Phone interface {
	call()
}
type NPhone struct {
}

func (np1 NPhone)call(){
	fmt.Println("i am nphone")
}

type IPhone struct {
}

func (ip1 IPhone)call()  {
	fmt.Println("i am iphone")
}
func main() {
	var p Phone
	p=new(NPhone)
	p.call()
	p=new(IPhone)
	p.call()

}
//输出
i am nphone
i am iphone
//要注意的是golang的名字在前,类型在后




//一般而言,接口是定义了一组方法的集合,接口是不能被实例化的,一个类型可以实现多个接口

import "fmt"

type Person interface{
	getName()string
}
type Student struct{
	name string
	age int
}

func(stu *Student)getName()string{
	return stu.name
}

type Worker struct{
	name string
	age int
}

func(w *Worker)getName()string{
	return w.name
}

func main(){
	var p Person=&Student{
		name:"Tom",//这里不要忘了逗号
		age:18,//这里也不要忘了,如果没写会报错。所以可以全部都写,
	}
	fmt.Println(p.getName())//Tom
}

//Go 语言中,并不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可。
//实例化 Student后,强制类型转换为接口类型 Person。
//我们在 main 函数中尝试将 Student 实例类型转换为 Person,如果 Student 没有完全实现 Person 的方法,比如我们将 (*Student).getName() 删掉,编译时会出现如下报错信息
//	*Student does not implement Person (missing getName method)

//但是删除 (*Worker).getName() 程序并不会报错,因为我们并没有在 main 函数中使用。这种情况下我们如何确保某个类型实现了某个接口的所有方法呢?一般可以使用下面的方法进行检测,如果实现不完整,编译期将会报错。
var _Person=(*Student)(nil)
var _Person=(*Worker)(nil)
//将空值 nil 转换为 *Student 类型,再转换为 Person 接口,如果转换失败,说明 Student 并没有实现 Person 接口的所有方法。Worker 同上。



//实例可以强制类型转换为接口,接口也可以强制类型转换为实例
func main(){
    var p Person=&Student{//实例转为接口
        name:"Tom",
        age:18,//都有逗号
    }
    stu:=p.(*Student)//接口转为实例
    fmt.Println(stu.getAge())
}



//空接口
如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型。

func main(){
    m:=make(map[string]interface{})
    m["name"]="Tom"
    m["age"]=18
    m["scores"]=[3]int{99,98,5}
    fmt.Println(m) //map[age:18 name:Tom scores:[99,98,5]]
}






错误处理

//Go 语言通过内置的错误接口提供了非常简单的错误处理机制。

//error类型是一个接口类型,这是它的定义
type error interface{
    Error() string
}
//我们可以在编码中通过实现 error 接口类型来生成错误信息。
//函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息
func Sqrt(f float64)(float64,error){
    if f<0{
        return 0,errors.New("math:square root of negative number")
    }
}
//在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码
result, err:= Sqrt(-1)

if err != nil {
   fmt.Println(err)
}

//例子
package main
import (
	"fmt"
)

// 定义一个 DivideError 结构
type DivideError struct {
	dividee int
	divider int
}

// 实现 `error` 接口
func (de *DivideError) Error() string {
	strFormat := `
    Cannot proceed, the divider is zero.
    dividee: %d
    divider: 0
`
	return fmt.Sprintf(strFormat, de.dividee)
}

// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
	if varDivider == 0 {
		dData := DivideError{
			dividee: varDividee,
			divider: varDivider,
		}
		errorMsg = dData.Error()
		return
	} else {
		return varDividee / varDivider, ""
	}

}

func main() {

	// 正常情况
	if result, errorMsg := Divide(100, 10); errorMsg == "" {
		fmt.Println("100/10 = ", result)
	}
	// 当除数为零的时候会返回错误信息
	if _, errorMsg := Divide(100, 0); errorMsg != "" {
		fmt.Println("errorMsg is: ", errorMsg)
	}

}
//输出
100/10 =  10
errorMsg is:  
    Cannot proceed, the divider is zero.
    dividee: 100
    divider: 0

并发

//go提供了sync和channel两种方式来支持协程(goroutine)的并发
例如我们希望并发下载 N 个资源,多个并发协程之间不需要通信,那么就可以使用 sync.WaitGroup,等待所有并发协程执行结束。
import (
	"fmt"
	"sync"
	"time"
)
var wg sync.WaitGroup
func download(url string){
	fmt.Println("start to download",url)
	time.Sleep(time.Second)//模拟耗时操作
	wg.Done()
}

func main(){
	for i:=0;i<3;i++{
		wg.Add(1)//为wg添加一个计数,wg.Done()会减去一个计数
		go download("a.com/"+string(i+'0'))//启动新的协程并发执行download函数
	}
	wg.Wait()//等待所有协程执行结束
	fmt.Println("Done!")
}




//Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
//goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

go 函数名(参数列表)
go f(x,y,z)

//开启一个新的goroutine
ff(x,y,z)

//Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。


package main

import (
	"fmt"
	"time"
)

func say(s string){
	for i:=0;i<5;i++{
		time.Sleep(100*time.Millisecond)//Millisecond是1ms,也就是1000微秒。这就是说睡眠0.1s
		fmt.Println(s)
	}
}
func main() {
	go say("world")//开了第一个线程
	say("hello")//又开了一个线程
}

//输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 


信道 channel


//channel
//通道(channel)是用来传递数据的一个数据结构。使用 channel 信道,可以在协程之间传递消息。阻塞等待并发协程返回消息
//通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。、、

ch<-v //把b发送到通道ch
V:=<-ch//从ch接受数据,并把值赋给v

//声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建
ch:=make(chan int)
//注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。


//实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:


func sum(s []int,c chan int)  {
	var sum int=0
	for _,v:=range s{
		sum+=v
	}
	c<-sum //把sum发给c
}

func main() {
	s:=[]int{7,2,8,-9,4,0}
	c:=make(chan int)
	go sum(s[:len(s)/2],c)
	go sum(s[len(s)/2:],c)
	x,y:=<-c,<-c  //都从通道c中接收
	fmt.Println(x,y,x+y)
}

//输出17 -5 12


var ch=make(chan string,10)//大小为10的字符串缓存信道
func download(url string){
    fmt.Println("start to download",url)
    time.Sleep(time.Second)
    ch<-url//把url发送给信道
}
func main(){
    for i:=0;i<3;i++{
        go download("a.com/"+string(i+'0'))//并发下载
    }
    for i:=0;i<3;i++{
        msg:=<-ch//等待信道返回消息
        fmt.Println("finish",msg)
    }
    fmt.Println("Done!")
}

//输出
start to download a.com/2
start to download a.com/0
start to download a.com/1
finish a.com/1
finish a.com/2
finish a.com/0
Done!



//通道缓冲区
//通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
chan:=make(chan int,100)
//带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

//不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

//注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

func main() {
ch:=make(chan int,2)
//因为ch是带缓冲区的通道,我们可以同时发送两个数据,而不用立刻去同步读取数据
ch<-1
ch<-2
//获取这俩数据
fmt.Println(<-ch)
fmt.Println(<-ch)
}
//输出 1 2




//go的遍历通道和关闭通道
//Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片
v,ok:=<-ch
//如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭
func fibonacci(n int, c chan int){
	x,y:=0,1
	for i:=0;i<n;i++{
		c<-x
		x,y=y,x+y
	}
	close(c)
}


func main() {
c:=make(chan int,10)
go fibonacci(cap(c),c)
	// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
	// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
	// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
	// 会结束,从而在接收第 11 个数据的时候就阻塞了。
	for i:=range c{
		fmt.Println(i)
	}
}

异常处理

//如果函数实现过程中,如果出现不能处理的错误,可以返回给调用者处理。比如我们调用标准库函数os.Open读取文件,os.Open 有2个返回值,第一个是 *File,第二个是 error, 如果调用成功,error 的值是 nil,如果调用失败,例如文件不存在,我们可以通过 error 知道具体的错误信息
import (
	"fmt"
	"os"
)

func main() {
    _, err := os.Open("filename.txt")//os.Open(*File,error)
    //这里是忽略了第一个参数*File
	if err != nil {
		fmt.Println(err)
	}
}
// open filename.txt: no such file or directory





//可以通过 errorw.New 返回自定义的错误

import (
	"errors"
	"fmt"
)

func hello(name string)error{//error可以作为返回值的类型
	if len(name)==0{
		return errors.New("error:name is null")
	}
	
	fmt.Println("hellow",name)
	return nil
}
func main(){
	if err:=hello("");err!=nil{
		fmt.Println(err)
	}
}




//error 往往是能预知的错误,但是也可能出现一些不可预知的错误,例如数组越界,这种错误可能会导致程序非正常退出,在 Go 语言中称之为 panic。
func get(index int) int {
	arr := [3]int{2, 3, 4}
	return arr[index]
}

func main() {
	fmt.Println(get(5))
	fmt.Println("finished")
}
//panic: runtime error: index out of range [5] with length 3
//goroutine 1 [running]:
//exit status 2



//java python中有try...catch机制,在try中捕获异常
//go 中对应的是defer..recover
defer func(){
   ....
}()


func get(index int)(ret int){
	defer func(){
		if r:=recover();r!=nil{
			fmt.Println("some error happen111",r)
			ret=-1
		}
	}()
	arr:=[3]int{2,3,4}
	return arr[index]
}
/* some error happen111 runtime error: index out of range [5] with length 3
-1
finished
*/

//	在 get 函数中,使用 defer 定义了异常处理的函数,在协程退出前,会执行完 defer 挂载的任务。因此如果触发了 panic,控制权就交给了 defer。
// 	在 defer 的处理逻辑中,使用 recover,使程序恢复正常,并且将返回值设置为 -1,在这里也可以不处理返回值,如果不处理返回值,返回值将被置为默认值 0。

单元测试(unit test)

假设我们希望测试 package main 下 calc.go 中的函数,要只需要新建 calc_test.go 文件,在calc_test.go中新建测试用例即可。

//calc.go
package main
func add(num1 int,num2 int)int{
    return num1+num2
}


//calc_test.go
package main
import "testing"
func TestAdd(t *testing.T){
    if ans:=add(1,2);ans!=3{
        t.Error("Add(1,2) should be equal to 3")
    }
}

//运行go test
$ go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      example 0.040s

包(package)

一般来说,一个文件夹可以作为 package,同一个 package 内部变量、类型、方法等定义可以相互看到。

比如我们新建一个文件 calc.go, main.go 平级,分别定义 add 和 main 方法

Go 语言也有 Public 和 Private 的概念,粒度是包。如果类型/接口/方法/函数/字段的

首字母大写,则是 Public 的,对其他 package 可见,

如果首字母小写,则是 Private 的,对其他 package 不可见。

新建一个文件 calc.go, main.go 平级,分别定义 add 和 main 方法。
// calc.go
package main

func add(num1 int, num2 int) int {
	return num1 + num2
}

// main.go
package main

import "fmt"

func main() {
	fmt.Println(add(3, 5)) // 8
}


运行 go run main.go,会报错,add 未定义:
./main.go:6:14: undefined: add
因为 go run main.go 仅编译 main.go 一个文件,所以命令需要换成
$ go run main.go calc.go

或者$ go run .



modules

Go Modules 是 Go 1.11 版本之后引入的,Go 1.11 之前使用 $GOPATH 机制。Go Modules 可以算作是较为完善的包管理工具。同时支持代理,国内也能享受高速的第三方包镜像服务。接下来简单介绍 go mod 的使用。Go Modules 在 1.13 版本仍是可选使用的,环境变量 GO111MODULE 的值默认为 AUTO,强制使用 Go Modules 进行依赖管理,可以将 GO111MODULE 设置为 ON。

在一个空文件夹下,初始化一个module
$ go mod init example
go:creating new go.mod:module example
此时,在当前文件夹下生成了go.mod,这个文件记录当前模块的模块名以及所有依赖包的版本。

这里是go 1.16

接着,我们在当前目录下新建文件 main.go,添加如下代码
package main

import (
	"fmt"

	"rsc.io/quote"
)

func main() {
	fmt.Println(quote.Hello())  // Ahoy, world!
}

//运行go run. 将会自动触发第三方包 rsc.io/quote的下载,具体的版本信息也记录在了go.mod中:
module example

go 1.13

require rsc.io/quote v3.1.0+incompatible


我们在当前目录,添加一个子 package calc,代码目录如下:
demo/
   |--calc/
      |--calc.go
   |--main.go


在 calc.go 中写入
package calc

func Add(num1 int, num2 int) int {
	return num1 + num2
}package main 中如何使用 package cal 中的 Add 函数呢?import 模块名/子目录名 即可,修改后的 main 函数如下:
package main

import (
	"fmt"
	"example/calc"

	"rsc.io/quote"
)

func main() {
	fmt.Println(quote.Hello())
	fmt.Println(calc.Add(10, 3))
}


$ go run .
Ahoy, world!
13

GIN

GIN简介

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance – up to 40 times faster. If you need smashing performance, get yourself some Gin.

Gin 是使用 Go/golang 语言实现的 HTTP Web 框架。接口简洁,性能极高。截止 1.4.0 版本,包含测试代码,仅14K,其中测试代码 9K 左右,也就是说框架源码仅 5K 左右。

GIN特性

  • 快速:路由不使用反射,基于Radix树,内存占用少。
  • 中间件:HTTP请求,可先经过一系列中间件处理,例如:Logger,Authorization,GZIP等。这个特性和 NodeJs 的 Koa 框架很像。中间件机制也极大地提高了框架的可扩展性。
  • 异常处理:服务始终可用,不会宕机。Gin 可以捕获 panic,并恢复。而且有极为便利的机制处理HTTP请求过程中发生的错误。
  • JSON:Gin可以解析并验证请求的JSON。这个特性对Restful API的开发尤其有用。
  • 路由分组:例如将需要授权和不需要授权的API分组,不同版本的API分组。而且分组可嵌套,且性能不受影响。
  • 渲染内置:原生支持JSON,XML和HTML的渲染

GIN踩坑

因为对着iris文档操作,开始学习,总是能遇到一些语法上的问题,每次一个小问题能折腾半天

想想还是做个记录吧。

1,cannot refer to unexported name controller.test

模块中要导出的函数,必须首字母大写。

2,not enough arguments in call to controller.Test
have ()

需要传入对应的参数 不然就报错

3,listen tcp: address 8088: missing port in address

原来是我在 8080前面少写了一个 :

4,none declared and not used

未申报未使用 没有定义 也没有使用

5,controller.Test() used as value

没有在控制器return

6,too many arguments to return
have (*tools.ResponseBean)
want ()

、这里指在函数返回另外一个类参数,其实是不用返回???我表示不懂

go里,函数有返回值则必须声明,而有返回值的函数就必须返回。。。go可是强类型语言啊。。。

编程小白遇到的错,有毒了! 在这里报错是因为没声明某些函数的用法,或者是用法错误

7,main.go:27:13: undefined: sql

因为没有import

“database/sql”
“github.com/go-sql-driver/mysql” 同时引入

8, 安装go get go.etcd.io/etcd/clientv3 报错
\pkg\mod\github.com\coreos\etcd@v3.3.25+incompatible\clientv3\balancer\picker\err.go:37:44: undefined: b alancer.PickOptions

在go.mod 里面添加 这一行

google.golang.org/grpc v1.26.0

再次执行 go get go.etcd.io/etcd/clientv3

可以成功安装

安装gin

  • 安装 Go (Ubuntu)
$ sudo apt-get install golang-go
$ go version
# go version go1.6.2 linux/amd64

Ubuntu自带版本太老了,安装新版可以使用如下命令。

$ sudo add-apt-repository ppa:gophers/archive
$ sudo apt-get update
$ sudo apt-get install golang-1.11-go

默认安装在/usr/lib/go-1.11,需要将/usr/lib/go-1.11/bin手动加入环境变量。在 .bashrc 中添加下面的配置,并 source ~/.bashrc

export PATH=$PATH:/usr/lib/go-1.11/bin
  • 安装 Go (Mac)
$ brew install go
$ go version
# go version go1.12.5 darwin/amd64
  • 设置环境变量

在 ~/.bashrc 中添加 GOPATH 变量

export GOPATH=~/go
export PATH=$PATH:$GOPATH/bin

添加完后,source ~/.bashrc

  • 安装一些辅助的工具库

由于网络原因,不能够直接访问 golang.org,但相关的库已经镜像到 Golang - Github

例如,直接安装 go-outline 时会报网络错误,因为golang.org/x/toolsgo-outline的依赖库。

$ go get -u -v github.com/ramya-rao-a/go-outline
github.com/ramya-rao-a/go-outline (download)
Fetching https://golang.org/x/tools/go/buildutil?go-get=1
https fetch failed: Get https://golang.org/x/tools/go/buildutil?go-get=1: 
dial tcp 216.239.37.1:443: i/o timeout

因此,可以先从 Github 手动安装好,再安装 go-outlinegoreturns

git clone https://github.com/golang/tools.git $GOPATH/src/golang.org/x/tools
go get -v github.com/ramya-rao-a/go-outline
go get -v github.com/sqs/goreturns
go get -v github.com/rogpeppe/godef

Go语言有大量的辅助工具,如果你使用VSCode,将会提示你将必要的工具,例如静态检查、自动补全等工具依次安装完毕。

  • 安装 Gin
go get -u -v github.com/gin-gonic/gin

-v:打印出被构建的代码包的名字
-u:已存在相关的代码包,强行更新代码包及其依赖包

第一个GIN程序

在一个空的文件夹中新建main.go
// geektutu.com
// main.go
package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.String(200, "Hello, Geektutu")
	})
	r.Run() // listen and serve on 0.0.0.0:8080
}

//	首先,我们使用了gin.Default()生成了一个实例,这个实例即 WSGI 应用程序。
//	接下来,我们使用r.Get("/", ...)声明了一个路由,告诉 Gin 什么样的URL 能触发传入的函数,这个函数返回我们想要显示在用户浏览器中的信息。
//	最后用 r.Run()函数来让应用运行在本地服务器上,默认监听端口是 _8080_,可以传入参数设置端口,例如r.Run(":9999")即运行在 _9999_端口。注意这个冒号

hello

First, create a Handler which receives all incomming HTTP connections from browsers, HTTP clients or API requests. A handler in Go is a function with this signature:

func (w http.ResponseWriter, r *http.Request)

The function receives two parameters:

  1. An http.ResponseWriter which is where you write your text/html response to.
  2. An http.Request which contains all information about this HTTP request including things like the URL or header fields.

Registering a request handler to the default HTTP Server is as simple as this:

http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
})

Listen for HTTP Connections

The request handler alone can not accept any HTTP connections from the outside. An HTTP server has to listen on a port to pass connections on to the request handler. Because port 80 is in most cases the default port for HTTP traffic, this server will also listen on it.

The following code will start Go’s default HTTP server and listen for connections on port 80. You can navigate your browser to http://localhost/ and see your server handing your request.

http.ListenAndServe(":80", nil)

The Code (for copy/paste)

This is the complete code that you can use to try out the things you’ve learned in this example.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
    })

    http.ListenAndServe(":80", nil)
}

路由

路由方法有:GET, POST, PUT, PATCH, DELETEOPTIONS,还有Any,可匹配以上任意类型的请求。

无参数
//无参数
r.GET("/",func(c *gin.Context){
    c.String(http.StatusOK,"Who are you?")
})
$ curl http://localhost:9999/user/geektutu
Hello geektutu

解析参数路径

有时候我们需要动态的路由,如 /user/:name,通过调用不同的 url 来传入不同的 name。/user/:name/*role* 代表可选

// 匹配 /user/geektutu
r.GET("/user/:name", func(c *gin.Context) {
	name := c.Param("name")
	c.String(http.StatusOK, "Hello %s", name)
})


//这时候我调用localhost:8090/user/xlk,他就会返回who are you? xlk?
r.GET("/user/:name",func(c *gin.Context){
		name:=c.Param("name")
		c.String(http.StatusOK,"who are you? %s ?",name)
	})
$ curl http://localhost:9999/user/geektutu
Hello geektutu

获取Query参数

// 匹配users?name=xxx&role=xxx,role可选
r.GET("/users", func(c *gin.Context) {
	name := c.Query("name")
	role := c.DefaultQuery("role", "teacher")
	c.String(http.StatusOK, "%s is a %s", name, role)
})
$ curl "http://localhost:9999/users?name=Tom&role=student"
Tom is a student
获取POST参数
// POST
r.POST("/form", func(c *gin.Context) {
	username := c.PostForm("username")
	password := c.DefaultPostForm("password", "000000") // 可设置默认值

	c.JSON(http.StatusOK, gin.H{
		"username": username,
		"password": password,
	})
})
$ curl http://localhost:9999/form  -X POST -d 'username=geektutu&password=1234'
{"password":"1234","username":"geektutu"}
$ curl http://localhost:9999/form  -X POST -d 'username=geektutu&password=1234'
{"password":"1234","username":"geektutu"}
Query和POST混合参数
// GET 和 POST 混合
r.POST("/posts", func(c *gin.Context) {
	id := c.Query("id")
	page := c.DefaultQuery("page", "0")
	username := c.PostForm("username")
	password := c.DefaultPostForm("username", "000000") // 可设置默认值

	c.JSON(http.StatusOK, gin.H{
		"id":       id,
		"page":     page,
		"username": username,
		"password": password,
	})
})
$ curl "http://localhost:9999/posts?id=9876&page=7"  -X POST -d 'username=geektutu&password=1234'
{"id":"9876","page":"7","password":"1234","username":"geektutu"}
Map参数(字典参数)
r.POST("/post", func(c *gin.Context) {
	ids := c.QueryMap("ids")
	names := c.PostFormMap("names")

	c.JSON(http.StatusOK, gin.H{
		"ids":   ids,
		"names": names,
	})
})
$ curl -g "http://localhost:9999/post?ids[Jack]=001&ids[Tom]=002" -X POST -d 'names[a]=Sam&names[b]=David'{"ids":{"Jack":"001","Tom":"002"},"names":{"a":"Sam","b":"David"}}
重定向(Redirect)
r.GET("/redirect", func(c *gin.Context) {
    c.Redirect(http.StatusMovedPermanently, "/index")
})

r.GET("/goindex", func(c *gin.Context) {
	c.Request.URL.Path = "/"
	r.HandleContext(c)
})
$ curl -i http://localhost:9999/redirect
HTTP/1.1 301 Moved Permanently
Content-Type: text/html; charset=utf-8
Location: /
Date: Thu, 08 Aug 2019 17:22:14 GMT
Content-Length: 36

<a href="/">Moved Permanently</a>.

$ curl "http://localhost:9999/goindex"
Who are you?
分组路由(Grouping Routes)

如果有一组路由,前缀都是/api/v1开头,是否每个路由都需要加上/api/v1这个前缀呢?答案是不需要,分组路由可以解决这个问题。利用分组路由还可以更好地实现权限控制,例如将需要登录鉴权的路由放到同一分组中去,简化权限控制。

// group routes 分组路由
defaultHandler := func(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"path": c.FullPath(),
	})
}
// group: v1
v1 := r.Group("/v1")
{
	v1.GET("/posts", defaultHandler)
	v1.GET("/series", defaultHandler)
}
// group: v2
v2 := r.Group("/v2")
{
	v2.GET("/posts", defaultHandler)
	v2.GET("/series", defaultHandler)
}
$ curl http://localhost:9999/v1/posts{"path":"/v1/posts"}$ curl http://localhost:9999/v2/posts{"path":"/v2/posts"}

上传文件

单个文件
r.POST("/upload1", func(c *gin.Context) {
	file, _ := c.FormFile("file")
	// c.SaveUploadedFile(file, dst)
	c.String(http.StatusOK, "%s uploaded!", file.Filename)
})
多个文件
r.POST("/upload2", func(c *gin.Context) {
	// Multipart form
	form, _ := c.MultipartForm()
	files := form.File["upload[]"]

	for _, file := range files {
		log.Println(file.Filename)
		// c.SaveUploadedFile(file, dst)
	}
	c.String(http.StatusOK, "%d files uploaded!", len(files))
})



// POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodPost, relativePath, handlers)
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值