为什么要学习go?
1)项目需要
go相对java的优势是什么?
1)并发支持的更好,支持轻量级的GOroutine和通信机制,并发编程更加简单高效
2)更高效,Golang 通过使用垃圾回收、内存池等技术,以及更高效的编译器和运行时环境,可以实现更高的性能。
3)更好的内存管理,Golang 的内存管理相对更为简单和可靠,其内存分配和回收是由编译器和运行时环境自动处理的
因为其在处理高并发、分布式系统和大规模数据等方面的优势,以及更高的性能和更易于部署和维护的特点,使其成为了开发云计算、大数据、网络服务等领域的热门选择。
首字母大小写区分作用域:
标识符(变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包);以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的)。
语法形式:
行尾一般不需要;分隔
但是如果一行有两个语句语句,则需使用;进行区分
go基础数据类型
布尔 bool
数字 int float32 float64
字符串
派生类型:指针 数组 结构化 channel 函数 切片 接口 Map
变量及其声明方式
// 方式一:指定变量类型
var b, c bool = true, false
// 方式二:根据值自行判定变量类型
var b = true
// 方式三:使用:=声明变量
b := true
值类型和引用类型
值类型:变量直接指向存在内存中的值,如var i int = 7, i直接指向堆中的值7 &i表示i的内存地址
引用类型:复杂的数据类型,如map、数组等,此时变量r1存储的是值所在的内存地址(数字),或内存地址中第一个字所在的位置。
如果执行r2=r1,则只有应用被复制,此时修改r1的值被改变,r2也会受到影响
go常量
常量是一个简单值的标识符,在程序运行时,不会被修改。
如: const b = "abc"
常量还可以用来定义枚举,如:
const (
Unknown = 0
Female = 1
Male = 2
)
iota:特殊常量,值可以被编译器修改,每当其被使用一次,值自动加1
如:
const (
a = iota //0
b //1
f = 100 //iota +=1
g //100 iota +=1
h = iota //4,恢复计数
i //5
)
go地址与指针运算符
& 取地址
* 指针变量
package main
import "fmt"
func main() {
var a int = 4
var ptr *int
/* & 和 * 运算符实例 */
ptr = &a /* 'ptr' 包含了 'a' 变量的地址 */
fmt.Printf("a 的值为 %d\n", a);
fmt.Printf("*ptr 为 %d\n", *ptr);
}
go条件表达式写法
if a < 20 {
fmt.Println("a 小于 20")
} else {
fmt.Println("a 不小于 20")
}
go循环语句
for i := 0; i <= 10; i++ {
sum += i
}
for ; sum <= 10; {
sum += sum
}
for循环读取数组:
numbers := [6]int{1, 2, 3, 5}
for i,x:= range numbers {
fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
}
for循环读取map的key和value
map1 := make(map[int]float32)
map[1] = 1.0
map[2] = 2.0
for key, value := range map1{
fmt.Printf("key is: %d - value is: %f\n", key, value)
}
函数
格式如下:
func function_name( [parameter list] ) [return_types] {
函数体
}
实例:
/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
/* 声明局部变量 */
var result int
if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}
函数调用:
var ret int = max(a, b)
函数的返回值可以是多个,如:
func swap(x, y string) (string, string) {
return y, x
}
函数作为实参
func main(){
/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
/* 使用函数 */
fmt.Println(getSquareRoot(9))
}
Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
以下实例中,我们创建了函数 getSequence() ,返回另外一个函数。该函数的目的是在闭包中递增 i 变量,代码如下:
实例
package main
import "fmt"
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
func main(){
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()
/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber()) //1
fmt.Println(nextNumber()) //2
fmt.Println(nextNumber()) //3
/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1()) //1
fmt.Println(nextNumber1()) //2
}
数组
声明 var balance[10] int
声明加初始化 balance := [5]int{1, 2, 3, 4, 5}
数组赋值 balance[2] = 3
访问数组元素 var salary int = balance[2]
指针
本质上也是一种数据类型,但是其存储的值是内存地址
使用指针的实例:
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a )
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
空指针:一个指针被定义后没有分配到任何变量时,它的值为 nil。
结构体
类似java中的dto
定义:
type Books struct {
title string
author string
subject string
book_id int
}
// 创建一个新的结构体对象
fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
// 也可以使用使用 key => value 格式,忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
var Book1 Books
Book1.title = "Python 教程"
fmt.Printf( "Book 1 title : %s\n", Book1.title)
切片
与数组类似,但切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
定义:slice1 = make([]T, length, capacity) 通过make()初始化函数
切片初始化: s := [] int {1, 2, 3}
从数组创建切片 s := arr[startIndex:endIndex] 将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片。
从切片创建切片 s1 := s[startIndex:endIndex] 通过切片 s 初始化切片 s1。
切片截取: s[1:4] /* 截取切片s从索引1(包含) 到索引4(不包含)*/
len()和cap()
var numbers = make([]int,3,5)
len(numbers) // 3
cap(numbers) // 5
空切片的值为nil
范围(range)
用于在for循环中迭代数组、切片、提供到、map等。
for循环迭代数组
var pow = []int{1, 2, 4, 8}
for i, v := range pow{
fmt.Println("2**%d = %d\n", i, v)
}
for循环迭代map
map1 := make(map[int]float32)
map1[1] = 1.0
map1[2] = 2.0
// 读取 key 和 value
for key, value := range map1 {
fmt.Printf("key is: %d - value is: %f\n", key, value)
}
map
创建 map1 := make(map[string]int)
实例:// 创建一个初始容量为 10 的 Map
m := make(map[string]int, 10)
m["apple"] = 5
// 获取键值对
v1 := m["apple"]
v2, ok := m["pear"] // 如果键不存在,ok 的值为 false,v2 的值为该类型的零值
// 删除键值对
delete(m, "banana")
类型转换
将一种数据类型的变量转换为另外一种类型的变量
整型转换为浮点型:
var a int = 10
var b float64 = float64(a)
字符串转换
var str string = "10"
var num int
num, _ = strconv.Atoi(str)
以上代码将字符串变量 str 转换为整型变量 num。
注意,strconv.Atoi 函数返回两个值,第一个是转换后的整型值,第二个是可能发生的错误,我们可以使用空白标识符 _ 来忽略这个错误
接口类型断言:将接口类型转换为指定类型
var i interface{} = "Hello, World"
str, ok := i.(string)
if ok {
fmt.Printf("'%s' is a string\n", str)
} else {
fmt.Println("conversion failed")
}
说明:定义了一个接口类型变量 i,并将它赋值为字符串 "Hello, World"。然后,我们使用类型断言将 i 转换为字符串类型,并将转换后的值赋值给变量 str。最后,我们使用 ok 变量检查类型转换是否成功,如果成功,我们打印转换后的字符串;否则,我们打印转换失败的消息。
接口类型转换:将一个接口类型的值转换为另一个接口类型
type Writer interface {
Write([]byte) (int, error)
}
type StringWriter struct {
str string
}
func (sw *StringWriter) Write(data []byte) (int, error) {
sw.str += string(data)
return len(data), nil
}
func main() {
var w Writer = &StringWriter{}
sw := w.(*StringWriter)
sw.str = "Hello, World"
fmt.Println(sw.str)
}
以上实例中,我们定义了一个 Writer 接口和一个实现了该接口的结构体 StringWriter。然后,我们将 StringWriter 类型的指针赋值给 Writer 接口类型的变量 w。接着,我们使用类型转换将 w 转换为 StringWriter 类型,并将转换后的值赋值给变量 sw。最后,我们使用 sw 访问 StringWriter 结构体中的字段 str,并打印出它的值。
接口
所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
接口是隐式实现的,也就是说,如果一个类型实现了一个接口定义的所有方法,那么它就自动地实现了该接口。
将接口作为参数来实现对不同类型的调用,从而实现多态。多态实例如下:
package main
import (
"fmt"
)
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()
}
错误处理
通过内置的错误接口提供了非常简单的错误处理机制。
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)
}
完整的使用实例如下:
// 定义一个 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` 类型除法运算的函数,里面使用DivideError返回错误信息
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, ""
}
}
并发
通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
实例如下:
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
// 这里开启异步,所以输出的hello和world是没有固定先后顺序。因为是两个goroutine在执行
go say("world")
say("hello")
}
channel
用来传递数据的一个数据结构,一般用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。
操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
通道定义: ch := make(chan int)
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。
以下实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 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)
}
通道缓冲区
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
实例:
package main
import "fmt"
func main() {
// 这里我们定义了一个可以存储整数类型的带缓冲通道
// 缓冲区大小为2
ch := make(chan int, 2)
// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2
// 获取这两个数据
fmt.Println(<-ch)
fmt.Println(<-ch)
}
遍历通道与关闭通道
Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:
v, ok := <-ch
如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。
实例
package main
import (
"fmt"
)
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)
}
}
执行输出结果为:
0
1
1
2
3
5
8
13
21
34