GO语言快速入门
简介
特性
强类型、编译型、并发型,并具有垃圾回收功能的编程语言。
运行方式
直接运行:go run helloworld.go
编译后再运行:
$ go build helloworld.go
$ ./helloworld
Hello,world
语法
- “{”必须和func同一行
- 首字母大写的是可以由package外部访问的;首字母小写的只能在package内部访问
数据类型
类型 | 名称 | 长度 | 零值 | 说明 |
---|---|---|---|---|
bool | 布尔类型 | 1 | false | 其值不为真即为假,不可以用数字代表true或false |
byte | 字节型 | 1 | 0 | uint8别名 |
rune | 字符类型 | 4 | 0 | 专用于存储unicode编码,等价于uint32 |
int, uint | 整型 | 4或8 | 0 | 32位或64位 |
int8, uint8 | 整型 | 1 | 0 | -128 ~ 127, 0 ~ 255 |
int16, uint16 | 整型 | 2 | 0 | -32768 ~ 32767, 0 ~ 65535 |
int32, uint32 | 整型 | 4 | 0 | -21亿 ~ 21 亿, 0 ~ 42 亿 |
int64, uint64 | 整型 | 8 | 0 | |
float32 | 浮点型 | 4 | 0 小数位精确到7位 | |
float64 | 浮点型 | 8 | 0 | 小数位精确到15位 |
complex64 | 复数类型 | 8 | ||
complex128 | 复数类型 | 16 | ||
uintptr | 整型 | 4或8 | ⾜以存储指针的uint32或uint64整数 | |
string | 字符串 | “” | utf-8字符串 |
变量声明
第一种,指定变量类型,声明后若不赋值,使用默认值。
var v_name v_type
v_name = value
第二种,根据值自行判定变量类型。
var v_name = value
第三种,省略var, 注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误。
v_name := value
// 例如
var a int = 10
var b = 10
c := 10
多变量声明
var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3
a, b, c := 5, 7, "abc"
如果你在定义变量 a 之前使用它,则会得到编译错误 undefined: a。
如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误
如果你想要交换两个变量的值,则可以简单地使用 a, b = b, a,两个变量的类型必须是相同。
空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃。
常量
const identifier [type] = value
iota
iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
const (
a = iota // 0
b // 1
c // 2
)
const (
i=1<<iota // 1
j=3<<iota // 3 * 2 =6
k // 3 * 4 = 12
l // 3 * 8 = 24
)
运算符
- 只有后自增和后自减 a++
流程控制
if
// if 后不用括号
if a < 20 {
fmt.Printf("a 小于 20\n" )
}
switch
// case后不用break。自动break 如果要继续下一个case则需要fallthrough
switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}
switch {
case grade == "A" :
fmt.Printf("优秀!\n" )
case grade == "B", grade == "C" :
fmt.Printf("良好\n" )
case grade == "D" :
fmt.Printf("及格\n" )
case grade == "F":
fmt.Printf("不及格\n" )
default:
fmt.Printf("差\n" );
}
//支持多条件匹配
switch a {
case 1,2,3,4:
default:
}
循环
// 类似c的for
for init; condition; post { }
// 类似c的while
for condition { }
// range 对slice map array
for key, value := range oldMap {
newMap[key] = value
}
函数
func function_name( [parameter list] ) [return_types] {
函数体
}
函数可以作为值来传递
func (name MyStruct) imp() string{
print("这是实现方法的写法")
}
func sum(x int,y int) int{
print("这是正常写法")
}
传递方式
和其他语言不同的是,go语言在将数组名作为函数参数的时候,参数传递即是对数组的复制。在形参中对数组元素的修改都不会影响到数组元素原来的值。
在使用slice作为函数参数时,本质也是传值,但由于slice由指针、长度、容量组成,因此也可以看成传递一个地址拷贝,即将底层数组的内存地址复制给参数slice。这时,对slice元素的操作就是对底层数组元素的操作,但slice本身不改变,若在函数内append,底层数组改变,但传入的参数的外部的slice长度不变。
切片传递的时候,等效于从原始切片中再切了一次。原始切片slice和参数s切片的底层数组是一样的。因此修改函数内的切片,也就修改了数组。如果在函数内,append操作超过了原始切片的容量,将会有一个新建底层数组的过程,那么此时再修改函数返回切片,应该不会再影响原始切片。
slice或者array作为函数参数传递的时候,本质是传值而不是传引用。传值的过程复制一个新的切片,这个切片也指向原始变量的底层数组。(个人感觉称之为传切片可能比传值的表述更准确)。函数中无论是直接修改切片,还是append创建新的切片,都是基于共享切片底层数组的情况作为基础。也就是最外面的原始切片是否改变,取决于函数内的操作和切片本身容量。
因此要传引用的话还是需要用指针
func main() {
slice := make([]int, 2, 2)
for i := 0; i < len(slice); i++ {
slice[i] = i
}
fmt.Printf("slice %v %p \n", slice, &slice)
ret := changeSlice(slice)
fmt.Printf("slice %v %p, ret %v \n", slice, &slice, ret)
ret[1] = -1111
fmt.Printf("slice %v %p, ret %v \n", slice, &slice, ret)
}
func changeSlice(s []int) []int {
fmt.Printf("func s %v %p \n", s, &s)
s[0] = -1
s = append(s, 3)
s[1] = 1111
return s
}
//输出
slice [0 1] 0xc42000a1a0
func s [0 1] 0xc42000a200
slice [-1 1] 0xc42000a1a0, ret [-1 1111 3]
slice [-1 1] 0xc42000a1a0, ret [-1 -1111 3]
append之前操纵的是同一个数组
匿名函数
Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
数组
数组是值类型
在 Go 中数组是值类型而不是引用类型。这意味着当数组变量被赋值时,将会获得原数组(译者注:也就是等号右面的数组)的拷贝。新数组中元素的改变不会影响原数组中元素的值。
var variable_name [SIZE] variable_type
var balance [10] float32
// 初始化数组中 {} 中的元素个数不能大于 [] 中的数字。
// 如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
// 多维数组
var threedim [5][10][4]int
指针
// 当一个指针被定义后没有分配到任何变量时,它的值为 nil。
var ip *int
结构体
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
// 创建一个新的结构体
fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
// 也可以使用 key => value 格式
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})
// 忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
}
使用结构体指针访问结构体成员,也使用 “.” 操作符
Slice
切片本身不包含任何数据。它仅仅是底层数组的一个上层表示。对切片进行的任何修改都将反映在底层数组中。
当若干个切片共享同一个底层数组时,对每一个切片的修改都会反映在底层数组中。
// 可以声明一个未指定大小的数组来定义切片
var identifier []type
// 或使用make()函数来创建切片 capacity是可选参数
make([]T, length, capacity)
初始化
s :=[] int {1,2,3 } // cap=len=3 创建了一个长度为 3 的 int 数组,并返回一个切片给 c。
s := arr[startIndex:endIndex] // s是arr的引用 缺省startIndex时将表示从arr的第一个元素开始,缺省endIndex时将表示一直到arr的最后一个元素
len() cap()
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)
}
append() 和 copy()
numbers = append(numbers, 2,3,4)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
当新元素通过调用 append 函数追加到切片末尾时,如果超出了容量,append 内部会创建一个新的数组(没超的话直接改原来的)。并将原有数组的元素被拷贝给这个新的数组,最后返回建立在这个新数组上的切片。这个新切片的容量是旧切片的二倍(译者注:当超出切片的容量时,append 将会在其内部创建新的数组,该数组的大小是原切片容量的 2 倍。最后 append 返回这个数组的全切片,即从 0 到 length - 1 的切片)
切片保留对底层数组的引用。只要切片存在于内存中,数组就不能被垃圾回收。这在内存管理方便可能是值得关注的。假设我们有一个非常大的数组,而我们只需要处理它的一小部分,为此我们创建这个数组的一个切片,并处理这个切片。这里要注意的事情是,数组仍然存在于内存中,因为切片正在引用它。
解决该问题的一个方法是使用 copy 函数 func copy(dst, src []T) int 来创建该切片的一个拷贝。这样我们就可以使用这个新的切片,原来的数组可以被垃圾回收。
range
用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素
nums := []int {2, 3, 4}
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
for i, c := range "go" {
fmt.Println(i, c)
}
map
var countryCapitalMap map[string]string /*创建集合 */
countryCapitalMap = make(map[string]string)
captial, ok := countryCapitalMap [ "美国" ]
delete(countryCapitalMap, "France") // 参数为map和key
类型转换
var sum int = 17
var count int = 5
var mean float32
mean = float32(sum)/float32(count)
fmt.Printf("mean 的值为: %f\n",mean) // 3.400000
接口
- 接⼝命名习惯以 er 结尾
- 接口只有方法声明,没有实现,没有数据字段
- 如果一个类型实现了一个接口要求的所有方法,那么该类型实现了这个接口,无须声明实现哪个接口。
- 接口的赋值规则:仅当一个表达式实现了一个接口时,这个表达式才可以赋值给该接口
- 空接口(interface{})不包含任何的方法,正因为如此,所有的类型都实现了空接口
type Phone interface {
call()
}
type NokiaPhone struct {
}
// 方法
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
var phone Phone
phone = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
}
类型断言
把对象赋给接口之后,若要得到它原来的类型,可以用类型断言。类型断言是一个作用于接口对象上的操作,写出来类似 x.(T),其中x是接口对象,而T是一个类型。
var v1 interface{} = 1
value, ok := v1.(int) // 1 true
错误处理
使用errors.New 可返回一个错误信息
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// 实现
}
result, err:= Sqrt(-1)
if err != nil {
fmt.Println(err)
}
并发
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
等待
sync.waitGroup
:等待所有goroutine执行完成,并且阻塞主线程的执行,直到所有的goroutine执行完成
GOMAXPROCS
调用 runtime.GOMAXPROCS() 用来设置可以并行计算的CPU核数的最大值,并返回之前的值。
通道(channel)
是用来传递数据的一个数据结构。从设计上确保,在同一时刻只有一个 goroutine 能从中接收或放入数据。发送和接收都是原子操作,不会中断。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
用途:
- 使用通道来同步goroutine
- 使用异步管道来保护临界资源(令牌 谁拿到了才能用,使用完退回管道)
- 用管道来发送事件
- …
同步通道
缓冲长度为0的channel称为同步管道,可以用来同步两个routine
- 发送操作被阻塞,直到接收端准备好接收
- 接收操作被阻塞,直到发送端准备好发送
异步通道
缓冲长度大于0的channel称为异步管道。
异步 channel,就是给 channel 设定个 buffer 值
- 在 buffer 未填满的情况下 ,不阻塞发送操作。
- 在buffer 未读完前,不阻塞接收操作。
// 声明通道
ch := make(chan int)
var ch1 chan int // ch1是一个正常的channel,不是单向的
var ch2 chan<- float64 // ch2是单向channel,只用于写float64数据
var ch3 <-chan int // ch3是单向channel,只用于读取int数据
// 通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小
ch := make(chan int, 2)
// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2
// 获取这两个数据
fmt.Println(<-ch)
fmt.Println(<-ch)
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)
}
}
select
每个case语句里必须是一个IO操作
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
defer
defer后面的函数在defer语句所在的函数执行结束的时候会被调用;
defer语句:一个普通的函数或方法调用,在调用前加上关键字 defer
函数和参数表达式会在语句执行时求值,但是无论在正确情况(如执行return语句等)还是非正常情况(如宕机),实际的调用推迟到包含defer语句的函数结束后才执行。
defer语句经常使用于成对的操作,比如打开和关闭、连接和断开、加锁和解锁,即使是再复杂的控制流、资源在任何情况下都能正确释放
如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
标准库
- 输入输出。这个分类包括二进制以及文本格式在屏幕、键盘、文件以及其他设备上的输
入输出等,比如二进制文件的读写。对应于此分类的包有bufio、 fmt、 io、 log和flag
等,其中 flag 用于处理命令行参数。 - 文本处理。这个分类包括字符串和文本内容的处理,比如字符编码转换等。对应于此分
类的包有encoding、 bytes、 strings、 strconv、 text、 mime、 unicode、 regexp、
index和path等。其中path用于处理路径字符串。 - 网络。这个分类包括开发网络程序所需要的包,比如Socket编程和网站开发等。对应于此
分类的包有: net、 http和expvar等。 - 系统。这个分类包含对系统功能的封装,比如对操作系统的交互以及原子性操作等。对
应于此分类的包有os、 syscall、 sync、 time和unsafe等。 - 数据结构与算法。对应于此分类的包有math、 sort、 container、 crypto、 hash、
archive、 compress和image等。因为image包里提供的图像编解码都是算法,所以也
归入此类。 - 运行时。对应于此分类的包有: runtime、 reflect和go等。
面向对象
方法
func后面加一个参数表示方法
Go可以将方法绑定到任何类型上,可以很方便的为简单的类型(如 int等)定义附加的方法
与普通函数声明类似,只是在函数名字前面多了一个参数,该参数把方法绑定到这个参数对应的类型上
type Point struct { X, Y float64}
func Distance (p, q Point) float64 { /* 普通函数 */
return math.Hypot(q.X-p.X,q.Y-p.Y)
}
func (p Point) Distance (q Point) float64 { /*Point 类型的方法*/
return math.Hypot(q.X-p.X,q.Y-p.Y)
}
p称为方法的接收者(可以是类型的值或是指针),接收者不使用特殊名(如this),而可以选择接收者名字
用指针接收者
func (p *Point) ScaleBy (factor float64) {
p.X *= factor
p.Y *= factor
}
习惯上遵循如果Point的任何一个方法使用指针接收者,那么所有的Point方法都应该使用指针接收者,用指针的调用方法:
p := Point{1, 2}
(&p).ScaleBy(2)
p.ScaleBy(2) //编译器会对变量进行 &p 的隐式转换,只有变量才允许这么做
// 如下面调用是错误的
Point{1, 2}.ScaleBy(2)
不允许本身是指针的类型进行方法声明
没有指向指针的用法
方法变量
一个函数,把方法绑定到一个接收者上,函数只需要提供实参而不需要提供接收者就能调用
p := Point{1, 2}
q := Point{2, 4}
distanceFromP := p.Distance //distanceFromoP 就叫做方法变量
distanceFromP(q)
方法表达式
T.f 或者(*T).f,其中T是类型
是一种函数变量
把原来方法的接收者替换为函数的第一个形参
distance := Point.Distance //方法表达式
distance(p, q)
只支持封装,不支持继承和多态,go中用面向接口来做继承和多态的任务
go中没有class ,只有struct
不支持重载
构造函数用工厂方法
反射
在编译时不知道类型的情况下,更新变量、在运行时查看值、调用方法以及直接对它们的布局进行操作的机制,叫做反射
反射功能由reflect包提供,定义了两个重要的类型
-
Type
- 有很多方法的接口,这些方法可以用来识别类型以及透视类型的组成部分。
- 接口只有一个实现,即类型描述符,接口值中的动态类型也是类型描述符。
- reflect.TypeOf函数接收任何的interface{}参数,并把接口中的动态类型以reflect.Type形式返回。
t := reflect.TypeOf(3) fmt.Println(t) // “int”
-
Value
包含一个任意类型的值
ValueOf函数接受任意的interface{}参数,并把接口的动态值以reflect.Value的形式返回。
stu := student{Name:"zhangsan", Age:25}
t := reflect.TypeOf(stu) //获取对象的类型名称fmt.Println("class name:", t.Name())
//获取stu对象的成员名称和值
v := reflect.ValueOf(stu)
for i := 0; i < t.NumField();i++{
field := t.Field(i)
fmt.Println(field.Name, "type:", field.Type)
fmt.Println(field.Name, "value:", v.Field(i))
}
//获取stu的方法
for i := 0; i < t.NumMethod(); i++ {
f := t.Method(i)
fmt.Println(f.Name, f.Type)
}
//设置stu的age值为100
v := reflect.ValueOf(&stu)
v = v.Elem()
field := v.FieldByName("Age")
field.SetUint(100)
//调用stu的setName方法
v = reflect.ValueOf(&stu)
m := v.MethodByName("SetName")
args := []reflect.Value{reflect.ValueOf("liSi")}
m.Call(args)
fmt.Println(stu)
其他
sync.WaitGroup
简单使用就是在创建一个任务的时候wg.Add(1), 任务完成的时候使用wg.Done()来将任务减一。使用wg.Wait()来阻塞等待所有任务完成。
注意点:传递给函数时要用地址传
上一篇: 5 函数二
下一篇: python新手学习第二天