鸣谢
此处先展示题目,后面会有题目解析。
for range
迭代map
是每次顺序是一样的吗?为什么?举例说明Printf(),Sprintf(),FprintF()
都是格式化输出,有什么不同?// 1. 程序声明:
import
package
// 2. 实体声明和定义:
chan // 通道
const // 常量声明
func // 函数声明
interface // 接口声明
map // map 声明
struct // 结构体声明
type // 类型声明
var // 变量声明
// 3. 流程控制
go // 开启协程
select //
break // 跳出循环
case // switch选择选项
continue // 跳出当前循环
default // switch默认区域
else // 条件判断
fallthrough //
for // 循环控制程序
goto // 并发控制程序运行函数
if // 条件结构程序
range // 循环遍历
return // 函数返回
switch // 选项选择
拿字符串来举例,可以有下面三种方式:
func main() {
var a string = "a" // 定义类型并赋值
b := "b" // 自动判断类型并赋初值
var c string // 只定义类型不赋初值
c = "c" // 变量后面随时可以修改
var d = "d" // 不指明类型的并赋初值
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
fmt.Println(d)
}
var a string = "a"
var b = "b"
c := "c"
var d string
d = "d"
func main() {
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
fmt.Println(d)
}
使用struct
可以定义结构体,具体可以参考如下的方式:
type Person struct {
name string
age int
}
func main() {
p1 := Person{"Alice", 20}
var p2 Person
p2.name "Panda"
p2.age = 24
fmt.Println(p1)
fmt.Println(p2)
}
1. 直接使用 ``:=`` 可以获取变量的地址
2. 用&xxxx来获取地址
func main() {
person := Person{"Alice", 20}
p1 := &person
fmt.Println(person)
fmt.Println(&person)
fmt.Println(&p1)
fmt.Println(*p1)
fmt.Println(person.name)
fmt.Println(&person.name)
fmt.Println(p1.name)
fmt.Println(&p1.name)
}
type Person struct {
name string
age int
}
- %T 类型
- %t 布尔
- %d 10进制整数
- %x 16进制整数
- %f 浮点数
- %s 字符串
person := Person{"Alice", 20}
fmt.Printf("%T\n", person)
flag := true
fmt.Printf("%t\n", flag)
number10 := 99
fmt.Printf("%d\n", number10)
//十进制100为16进制的64
number16:= 0x64
fmt.Printf("%X\n", number16)
fmt.Printf("%d\n", number16)
number0:= 0.123
fmt.Printf("%f\n", number0)
str:= "hello world"
fmt.Printf("%s\n", str)
一个类如果实现了一个接口的所有函数,那么这个类就实现了这个接口。
go接口
go 语言接口的作用是,可以实现OO面向对象的特性,从语法上看,Interface定义了一个或一组method(s),这些method(s)只有函数签名,没有具体的实现代码(有没有联想起C++中的虚函数?)。下面是一个运用的实例:
type MyInterface interface{
Print()
}
func TestFunc(x MyInterface) {}
type MyStruct struct {}
func (me MyStruct) Print() {}
func main() {
var me MyStruct
TestFunc(me)
}
具体可以参考: 五分钟理解golang init函数
init 是初始化函数,在包引入的时候就会调用,一个包可以写多个init函数。
init 函数的主要作用是:
init 函数的主要特点:
具体可以参考下面的例子:
package main
import (
"fmt"
)
var T int64 = a()
func init() {
fmt.Println("init in main.go ")
}
func a() int64 {
fmt.Println("calling a()")
return 2
}
func main() {
fmt.Println("calling main")
}
输出:
calling a()
init in main.go
calling main
初始化顺序:变量初始化->init()->main()
实例2:
func init() {
fmt.Println("init 1")
}
func init() {
fmt.Println("init 2")
}
func main() {
fmt.Println("main")
}
输出:
init 1
init 2
main
可以直接在函数参数列表里输入…,来定义多参数函数
func add(args ...int) int {}
// 这个函数的调用方式有:
add(1, 2, 3)
add([]int{1, 2, 3}...)
这样就可以传入多个参数来给函数了。
类似其他语言的强制类型转换,直接用类型名称(变量)
,就可以实现这样的效果。
举例如下:
type MyInt int
var a int = 1
var b MyInt = MyInt(a)
参考书籍:看云-golang语言类型介绍
Golang的引用类型包括 slice、map 和 channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。
能够让外部变量直接操作某块内存地址。
内置函数 new 计算类型大小,为其分配零值内存,返回指针。而 make 会被编译器翻译 成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针。
引用类型:
变量存储的是一个地址,这个地址所存储的值,
在内存上通常是将其分配在堆上面,
在程序中通过GC来进行回收。
获取指针类型的所指向的值,
可以使用: “*”取值符号。
比如 var *p int, 使用*p获取p指向的值
指针、slice、map、chan都是引用类型。
实现切片初始化的方式,大致我学习到的可以分为两种:
具体可以参考下面的代码:
func main() {
s1 := make([]int, 0)
s2 := make([]int, 6,10)
s3 := []int{1, 2, 3, 4, 5}
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s3)
fmt.Println(len(s2))
fmt.Println(cap(s2))
}
go语言当中函数的定义,大致我概括一下可以分为如下几类:
具体可以参考如下的代码:
// - 不带参数的函数定义
func main() {
r1, r2 := getResult(1, 2)
fmt.Println(r1)
fmt.Println(r2)
}
// - 带参数的函数定义
func getResult(a int, b int) (c int, d int) {
return a + b, a - b
}
// - 返回值有/无标识的函数定义
func getResult(a int, b int) (int, int) {
return a + b, a - b
}
func getResult(a int, b int) (c int, d int) {
return a + b, a - b
}
// - 多参数函数定义
func getResult(a int , b int , c ...int) (c int, d int) {
return a + b, a - b
}
// - 类成员函数定义
type Person struct {
name string
age int
}
func (p Person) setName(n string) {
if n == "" {
p.name = "NaN"
}
p.name = n
}
关于可变参数的讲解,可以参考: Go语言“可变参数函数”终极指南
关于go中面向对象定义类成员函数,可以参考: go中的面向对象
这里展示一块面向对象使用的代码:
package main
import "fmt"
type Human struct {
height float32
weight int
}
func (h Human) BMIindex() (index int){
index = h.weight / int(h.height * h.height)
return
}
func (h Human) setHeight(height float32) {
h.height = height
}
func main() {
person := Human{1.83, 75}
fmt.Printf("this person's height is %.2f m\n", person.height)
fmt.Printf("this person's weight is %d kg\n", person.weight)
fmt.Printf("this person's BMI index is %d\n", person.BMIindex())
person.setHeight(1.90)
fmt.Printf("this person's height is %.2f m\n", person.height)
}
输出结果:
this person's height is 1.83 m
this person's weight is 75 kg
this person's BMI index is 25
this person's height is 1.83 m
可以看出,我们调用person.setHeight(1.90)之后,person的height属性并没有改变为1.90。而为了解决这个问题,我们需要改变receiver。我们将setHeight()函数定义为下述形式即可。
func (h *Human) BMIindex() (index int){
index = h.weight / int(h.height * h.height)
return
}
同步锁的作用是保证资源在使用时的独有性,不会因为并发而导致数据错乱,保证系统的稳定性。
无缓冲的 channel是同步的,而有缓冲的channel是非同步的。
cap函数在讲引用的问题中已经提到,可以作用于的类型有:
详细的类型断言的学习,可以参考: Go语言圣经-类型断言
在go语言中断言的代码举例如下:
func main() {
m := make(map[int]interface{})
m[0] = Person{}
m[1] = "abc"
// 两个参数分别获得值和对应是否是相同类型
r1, r2 := m[0].(Person)
r3, r4 := m[1].(string)
fmt.Println(r1)
fmt.Println(r2)
fmt.Println(r3)
fmt.Println(r4)
}
输出结果为
{}
true
abc
true
一个类型断言的作用是:一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。
类型断言是一个使用在接口值上的操作。语法上它看起来像 x . ( T ) x.(T) x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。
在go中想要对切片进行元素的删除,具体过程如下:
func main() {
s := make([]string, 0)
s = append(s, "abc0")
s = append(s, "abc1")
fmt.Println(s)
s = append(s[:0], s[0+1:]...)
fmt.Println(s)
s = append(s[:0], s[0+1:]...)
fmt.Println(s)
}
// 输出结果
[abc0 abc1]
[abc1]
[]
在go当中,我们可以利用json库来对某些结构体进行相关的json格式化重命名的操作,来完成对应的数据类型的转换。
func main() {
p1 := Person{"Alice", 20}
fmt.Println(p1)
bytes, _ := json.Marshal(p1)
fmt.Println(string(bytes))
}
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
// 输出结果
{Alice 20}
{"name":"Alice","age":20}
我们通过在一个新的结构体里面,定义某个类型的数据的对象,就能够实现继承这个类的数据的操作。比如类似下面这样:
func main() {
stu := Student{Person{"Panda", 20}}
fmt.Println(stu)
}
type Person struct {
Name string
Age int
}
type Student struct {
Person
}
输出结果
{{Panda 20}}
for range
迭代map
是每次顺序是一样的吗?为什么?举例说明使用 for range
迭代map
时,每次迭代的顺序可能不一样,因为map
的得带是随机的
func main() {
m := make(map[string]int)
m["string"] = 1
m["int"] = 1
m["float"] = 2
m["bool"] = 3
m["byte"] = 4
for k, v := range m {
fmt.Println(k, ",", v)
}
// 打印的顺序会出现不一样的情况
}
一共有18个,主要有:
1. bool
2. string
3. byte
4. int
5. uint
6. float
go 当中switch语句和其他语言类似,只是有一个特殊的地方,switch后面可以不跟表达式
func main() {
i := rand.Intn(2)
switch i {
case 0:
fmt.Println("get 0")
case 1:
fmt.Println("get 1")
}
}
// switch后面可以不跟表达式
func main() {
i := rand.Intn(2)
switch {
case i == 0:
fmt.Println("get 0")
case i == 1:
fmt.Println("get 1")
}
}
结构体在序列化的时候非导出变量(以小写字母开头的变量名)不会被encode,所以在decode时这些非到处变量的值为其类型的零值。
new
的作用是初始化一个纸箱类型的指针new
函数是内建函数,函数定义:func new(Type) *Type
new
函数来分配空间new
函数的是一个类型,而不是一个值那么可以看一下代码
func main() {
p := new(Person)
person := Person{"Panda"}
fmt.Printf("%T\n",p)
fmt.Printf("%T\n",person)
}
type Person struct {
name string
}
// 输出结果为
// *main.Person
// main.Person
// 所以new函数返回的是指针
// person是实体
make
的作用是为slice, map or chan
的初始化
然后返回引用
make
函数是内建函数,函数定义:
func make(Type, size IntegerType) Type
make(T, args)
函数的目的和new(T)
不同
仅仅用于创建slice, map, channel
而且返回类西行是实例
Printf(),Sprintf(),FprintF()
都是格式化输出,有什么不同?虽然这三个函数,都是格式化输出,但是输出的目标不一样
Printf
是标准输出,一般是屏幕,也可以重定向。
Sprintf()
是把格式化字符串输出到指定的字符串中
func main() {
person := Person{"Alice"}
s := fmt.Sprintf("类型是%T\n", person)
fmt.Println(s)
}
Fprintf()
是吧格式化字符串输出到文件中,主要用于文件操作的代码:
file, e := os.OpenFile("f:1.txt", os.O_RDWR, 0777)
defer file.Close()
if e != nil {
fmt.Println(e)
}
fmt.Fprintln(file, "Hello, world")
数组:
[3]int
和[4]int
是两种不同的数组类型切片:
make()
来初始化,初始化的时候len=cap
,然后进行扩容具体我们可以参考如下的代码:
func main() {
p1 := Person{"Alice"}
p2 := Person{"Bob"}
change(p1)
changeAddress(&p2)
fmt.Println(p1)
fmt.Println(p2)
}
// 值传递
func change(p Person) {
p.name = "Hello"
}
// 地址传递
func changeAddress(p *Person) {
p.name = "Hello"
}
type Person struct {
name string
}
1. 数组是值传递
2. 切片是引用传递
具体可以参考下面的代码:
func main() {
arr1 := [3]int{1, 2, 3}
arr2 := []int{1, 2, 3}
changeArr(arr1)
changeSlice(arr2)
fmt.Printf("%T\n", arr1)
fmt.Printf("%T\n", arr2)
fmt.Println(arr1)
fmt.Println(arr2)
}
func changeArr(arr [3]int) {
arr[0] = 9
}
func changeSlice(arr []int) {
arr[0] = 9
}
结果为
[1 2 3]
[9 2 3]
file, err := os.OpenFile("f:/1.txt", os.O_RDWR, 0777)
defer file.Close()
if err != nil {
fmt.Println(err)
return
}
file.WriteString("hello world")
func main() {
arr := make([]int, 0)
for i := 0; i < 2000; i++ {
fmt.Println("len为", len(arr), "cap为", cap(arr))
arr = append(arr, i)
}
}
我们可以看下结果
依次是
0,1,2,4,8,16,32,64,128,256,512,1024
但到了1024之后,就变成了
1024,1280,1696,2304
每次都是扩容了四分之一左右
我们下来看一下使用代码:
func pase_student() {
m := make(map[string]*student)
stud := []student{
{Name:"zhou", Age:24},
{Name: "li", Age: 23},
{Name: "wang", Age: 22},
}
for _, stu := range stus {
m[stu.Name] = &stu
}
fmt.Println(m)
}
看下结果
map[zhou:0xc000004440 li:0xc000004440 wang:0xc000004440]
我们再看一段直观一点的代码
stus := []student{
{Name: "zhou", Age: 24},
{Name: "li", Age: 23},
{Name: "wang", Age: 22},
}
for _, stu := range stus {
fmt.Printf("%p\n",&stu)
}
结果是
0xc000052400
0xc000052400
0xc000052400
原因是foreach使用副本的方式,所以&stu实际上
指向的是同一个指针
最终该指针的值是最后一个struct的值的拷贝
正确的方法应该是这样
for i := 0; i < 3; i++ {
stu:=stus[i]
fmt.Printf("%p\n",&stu)
}
Go中GoMAXPROCS
在 Go语言程序运行时(runtime)实现了一个小型的任务调度器。这套调度器的工作原理类似于操作系统调度线程,Go 程序调度器可以高效地将 CPU 资源分配给每一个任务。传统逻辑中,开发者需要维护线程池中线程与 CPU 核心数量的对应关系。同样的,Go 地中也可以通过 runtime.GOMAXPROCS() 函数做到,格式为:
runtime.GOMAXPROCS(逻辑CPU数量)
这里的逻辑CPU数量可以有如下几种数值:
runtime.GOMAXPROCS 的作用是:调整并发的运行性能。
看下代码
func main() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 10; i++ {
go func() {
fmt.Println("A: ", i)
wg.Done()
}()
}
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println("B: ", i)
wg.Done()
}(i)
}
wg.Wait()
}
先看第一个循环
首先,for循环很快就结束了
然后开启10个go协程
所以输出了10个A:10
再看第二个循环
每次循环把i当做参数传入函数
但是go协程启动的顺序是不一定的
所以输出10个数字,顺序是不一定的
可以来看一段代码:
func (p *People) ShowA() {
fmt.Println("showA")
p.ShowB()
}
func (p *People) ShowB() {
fmt.Println("showB")
}
type Teacher struct {
People
}
func (t *Teacher) ShowB() {
fmt.Println("teacher showB")
}
func main() {
t := Teacher{}
t.ShowA()
}
首先,调用ShowA方法
那么会输出ShowA
然后People指针调用ShowB方法
这时候这个指针不知道自己是Teacher
所以还是走People的ShowB
所以结果是
ShowA
ShowB
看一段代码:
func main() {
runtime.GOMAXPROCS(1)
int_chan := make(chan int, 1)
string_chan := make(chan string, 1)
int_chan <- 1
string_chan <- "hello"
select {
case value := <-int_chan:
fmt.Println(value)
case value := <-string_chan:
panic(value)
}
}
我们看到select中的两个case都满足
那么会随机选择一个来执行
所以程序有可能崩溃,也有可能不会崩溃
defer讲解
defer的作用是:
defer的常用场景:
观察下面的程序中defer的执行顺序是什么
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
a := 1
b := 2
defer calc("1", a, calc("10", a, b))
a = 0
defer calc("2", a, calc("20", a, b))
b = 1
}
结果为
"10" 1 2 3
"20" 0 2 2
"2" 0 2 2
"1" 1 3 4
原因:
1. 函数calc调用的时候,b参数是使用的新的calc的返回值,所以先运行作为参数的calc
2. defer会造成延迟运行,所以main中定义的两个defer会按照与定义相反的顺序返回结果
func main() {
s := make([]int, 5)
s = append(s, 1, 2, 3)
fmt.Println(s)
// 结果为0,0,0,0,0,1,2,3
// make初始化是有默认值的,这里默认值是0
}
type UserAges struct {
ages map[string]int
sync.Mutex
}
func (ua *UserAges) Add(name string, age int) {
ua.Lock()
defer ua.Unlock()
ua.ages[name] = age
}
func (ua *UserAges) Get(name string) int {
if age, ok := ua.ages[name]; ok {
return age
}
return -1
}
这段代码有可能会出现
fatal error: concurrent map read and map write
原因是在读取的时候没有给数据源加锁
我们可以修改一下代码,保证线程安全
type UserAges struct {
ages map[string]int
sync.Mutex
}
func (ua *UserAges) Add(name string, age int) {
ua.Lock()
defer ua.Unlock()
ua.ages[name] = age
}
func (ua *UserAges) Get(name string) int {
ua.Lock()
defer ua.Unlock()
if age, ok := ua.ages[name]; ok {
return age
}
return -1
}
个人小结一下定义go中的缓冲池需要的步骤如下:
具体可以看如下的代码:
func (set *threadSafeSet) Iter() <-chan interface{} {
ch := make(chan interface{})
go func() {
set.RLock()
for elem := range set.s {
ch <- elem
}
close(ch)
set.RUnlock()
}()
return ch
}
完整的演示示例如下:
type threadSafeSet struct {
sync.RWMutex
s []interface{}
}
func (set *threadSafeSet) Iter() <-chan interface{} {
// ch := make(chan interface{}) // 解除注释看看!
ch := make(chan interface{}, len(set.s))
go func() {
set.RLock()
for elem, value := range set.s {
ch <- elem
fmt.Println("Iter:", elem, value)
}
close(ch)
set.RUnlock()
}()
return ch
}
func main() {
th := threadSafeSet{
s: []interface{}{"1", "2"},
}
v := <-th.Iter()
fmt.Sprintf("%s%v", "ch", v)
}
运行结果
Iter: 0 1
Iter: 1 2
type People interface {
Show()
}
type Student struct {}
func (stu *Student) Show() {
}
func live() People {
var stu *Student
return stu
}
func main() {
if live() == nil {
fmt.Println("AAAAAA")
} else {
fmt.Println("BBBBBB")
}
}
我们说一下interface
的内部结构
interface
分为两种
1.空接口
2.带方法的接口
var people interface{}
type People interface{
sayHello()
}
底层结构如下:
type eface struct { //空接口
_type *_type //类型信息
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type iface struct { //带有方法的接口
tab *itab //存储type信息还有结构实现方法的集合
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type _type struct {
size uintptr //类型大小
ptrdata uintptr //前缀持有所有指针的内存大小
hash uint32 //数据hash值
tflag tflag
align uint8 //对齐
fieldalign uint8 //嵌入结构体时的对齐
kind uint8 //kind 有些枚举值kind等于0是无效的
alg *typeAlg //函数指针数组,类型实现的所有方法
gcdata *byte
str nameOff
ptrToThis typeOff
}
type itab struct {
inter *interfacetype //接口类型
_type *_type //结构类型
link *itab
bad int32
inhash int32
fun [1]uintptr //可变大小 方法集合
}
可以看出iface比eface中间多了一层itab结构
itab存储_type信息和[]fun方法集
从上面的结构我们可以看出
data指向了nil
但是并不代表interface是nil
所以返回值不为空
这里的fun方法集定义了接口的接收规则
在编译的过程中需要验证是否实现接口
func main() {
var p1 Person
var p2 Person
p2.name= "Alice"
p3:=Person{}
fmt.Println(p1)
fmt.Println(p2)
fmt.Println(p3)
}
结果为
{}
{Alice}
{}
会出现随机的结果,因为go 启动的时机是随机的,所以 num打印和channel的打印顺序是随机交错的。
channel := make(chan int)
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
channel <- i
}()
}
for i := 0; i < 10; i++ {
num := <-channel
fmt.Println("num:", num)
}
go语言是Google开发的一种:
强类型指的是程序中表达的任何对象所从属的类型
都必须能在编译时刻确定
常见的强类型语言有
C++, Java, Python, Golang等
适合大规模信息系统开发
javascript是弱类型的
比如
var a = 1;
var b = 'a';
console.log(a+b)