IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> java与Go语言的相似之处 -> 正文阅读

[Java知识库]java与Go语言的相似之处

一、go struct静态函数、结构体、函数、方法、接口

编程语言都是相通的,go和c的结构体structure,相当于java的class类,slice切片相当于java的ArrayList,同样的也有map;指针就是一个地址引用。也有接口,只是C语言和go没有对象的概念,但是却有类型的概念的type关键字。

go的struct结构体

结构体struct就相当于java中的类class,用于定义属性和方法。

定义一个Person,有string型的Name和int型的Age两个属性:

type Person struct {
Name string
Ageint
}

创建一个Person实例:
第一种方式:

var p Person = Person{}

这时,所有属性值都是零值。
直接打点给属性赋值:

p.Name = "zhangsan"
p.Age = 18
var p Person = Person{"zhangsan", 18}

一个个把属性值列出来,要求一个都不能少。
第二种方式:

var p Person = Person{Name: "zhansan", Age: 18}

这时,Name属性值为zhangsan,Age属性值为18

把属性名与值一对对列出来,可以只列需要的属性,其他的属性值为零值。注意,属性名没有用双引号括住。

第三种方式:

var pptr *Person  = new(Person)

注意,与上面两种方式不同,用new函数生成的是指向实例的指针,注意,不是new关键字,而是new函数,java中是new关键字。new(Person)相当于&Person{},返回都是指针类型。
我们可以用reflect.TypeOf()函数来查看变量类型

func main() {
a := Person{"张三", 90}
log.Println(reflect.TypeOf(a))
var pptr *Person = new(Person)
log.Println(reflect.TypeOf(pptr))
}

会打印出

main.Person
*main.Person

可以看出,a是Person类型,pptr是指针类型,类型名以星号开头。

var p Person = new(Person)会报编译错误,提示Cannot use ‘new(Person)’ (type *Person) as type Person。

我们既可以通过实例访问其成员变量,又可以直接通过指向该实例的指针访问其成员变量

如pptr.Name = “lisi”

fmt.Println(pptr.Name)

定义一个Employee,有Person型的p和int型的salary两个属性:

type Employee struct {

p Person

salaryint}

创建一个Employee实例:

var e Employee = Employee{}

var e Employee = Employee{p: Person{}, salary: 10000}

var eptr *Employee = new(Employee)

如果属性类型是自定义的struct的话,属性名可以省略,如下:

type Employee struct {
Person
salaryint
}

此时创建一个Employee实例:

var e Employee = Employee{}
var e Employee = Employee{Person: Person{}, salary: 10000}

这里由于属性名省略了,所以花括号中的key只能是我们自定义的struct了。

var eptr *Employee = new(Employee)

通过e访问Name、Age属性:

在属性名不省略时,只能通过e打点获取Person实例,然后Person实例再打点操作Name、Age属性,示例如下:

func main() {

var e Employee= Employee{p: Person{}, salary: 10000}

e.p.Name= "zhangsan"fmt.Println(e)

}

在属性名省略时,我们既可以通过e打点获取Person实例,然后Person实例再打点操作Name、Age属性,也可以直接e打点操作Name、Age属性,这就有点像java的继承了。示例如下:

func main() {

var e Employee= Employee{Person: Person{}, salary: 10000}

e.Person.Name= "zhangsan"e.Age= 30fmt.Println(e)

}

函数

在go中,函数是一等公民。
函数可以有不定长入参,可以有多个返回值,可以赋值给变量,可以作为函数的入参和出参。

函数定义:

func func_name(i int, s string) int {}

定义函数f0,有一个int型入参、无出参:

func f0(i int) {

}

定义函数f1,有一个int型入参、一个string型入参,一个int型出参:

func f1(i int, s string) int{return 0}

定义函数f2,有两个int型入参,一个string型出参:

func f2(i, j int) string {return ""}

如果多个入参类型一样的话,可以省略前面几个参数的类型关键字,而只保留最后一个参数的类型关键字。

定义函数f3,有两个int型入参,一个string型出参,一个int型出参:

func f3(i, j int) (string, int) {return "", 0}

多个出参的话,要用括号包起来。

定义函数f4,有不定长个int型入参,一个int型出参:

func f4(p ...int) int{
returnlen(p)
}

定义函数f5,有一个string型入参和不定长个int型入参,一个int型出参:

func f5(s string, sl ...int) int{
return len(s) +len(sl)

}

函数赋值给变量:

func main() {
var f func(int, string) int =f1
f(1, "a")
}

变量的类型可以通过fmt.Println(reflect.TypeOf(f))打印出来看。

函数作为函数的入参和出参

func S(f func(i int) int) func(s string) string {
returnfunc(s string) string {returnstrconv.Itoa(f(len(s)))
}
}
func main() {
var p= S(func(i int) int{return i + 10})("100")
fmt.Println(p)
}

我们在定义函数时,如果入参是一个struct实例,则底层会复制一个struct实例作为入参,函数对实例的改动不会影响原来的struct实例。所以,入参最好是指向struct实例的指针,这样就不用复制了,函数对实例的改动也会体现到原来的struct实例上。示例如下:

func changeName(e Person) {
e.Name= "zhangsan"}
func changeNamePtr(e*Person) {
e.Name= "zhangsan"}
func main() {
var p Person=Person{}
changeName(p)
fmt.Println(p)
var ptr*Person = new(Person)
changeNamePtr(ptr)
fmt.Println(ptr)
}

方法

方法和函数长得差不多,区别是方法定义时func关键字后面紧跟的是括号,括号里面是调用者形参及调用者类型,之后才是func_name,再之后是括号,括号里面是入参形参及入参类型(没有入参的情况下括号不可以省略),最后是出参类型,如果有多个出参,则出参类型要用括号括住。

从上面描述可以看出,在方法定义时,方法的调用者类型就已经是确定了的,只有这个调用者类型的实例或者指针才能调用这个方法。方法定义时的调用者类型既可以是普通类型,也可以是指针类型,方法最终的调用者也是既可以是普通类型,也可以是指针类型。

示例如下:

type JavaProgrammer struct {
}
func (jp*JavaProgrammer) helloWorld() {
fmt.Println("System.out.println(\"Hello World!\");")

}

type GoProgrammer struct {

}
func (gp GoProgrammer) helloWorld() {

fmt.Println("fmt.Println(\"Hello World!\")")

}

func main() {new(JavaProgrammer).helloWorld()

javaProgrammer :=JavaProgrammer{}

javaProgrammer.helloWorld()new(GoProgrammer).helloWorld()

goProgrammer :=GoProgrammer{}

goProgrammer.helloWorld()

}

JavaProgrammer的 helloWorld方法是调用者类型定义成指针类型的方法,第一次调用是用指向JavaProgrammer实例的指针调用,第二次调用是用JavaProgrammer实例调用。

GoPrammer的helloWorld方法是调用者类型定义成普通类型的方法,第一次调用是用指向GoProgrammer实例的指针调用,第二次调用是用GoProgrammer实例调用。

相比来说,调用者类型定义成指针类型的方法要比定义成普通类型的方法好,因为在调用时可以避免内存复制。

func main() {

a := Person{"张三", 90}

log.Println(1, unsafe.Pointer(&a.Name))

a.Ps()

a.Ps2()

}
func (e Person) Ps() {
log.Println(0, unsafe.Pointer(&e.Name))
}
func (e*Person) Ps2() {

log.Println(0, unsafe.Pointer(&e.Name))
}

在Ps方法内部打印的入参a的Name属性地址和直接打印实例a的Name属性地址不一样,而在Ps2方法内部打印的入参a的Name属性地址和直接打印实例a的Name属性地址一样,由此可以证明调用者类型定义成指针类型的方法在调用时,不会有内存复制,而调用者类型定义成普通类型的方法在调用时,会有内存复制。

func main() {

a := Person{"张三", 90}

log.Println(1, unsafe.Pointer(&a))

a.Ps()

a.Ps2()

}

func (e Person) Ps() {

log.Println(0, unsafe.Pointer(&e))

}

func (e*Person) Ps2() {

log.Println(0, unsafe.Pointer(&e))

}

打印出来的三个值各不相同,难道说形参是指针类型的方法在调用时,也会有内存复制?

func (p *Person) exchange(p0 *Person) {}
  1. *第一个括号中的p Person表示本方法调用者只能是Person实例或者指向Person实例的指针,且是引用传递,如果在方法中改变了调用者的属性,会在方法外体现。

  2. *第二个括号中的p0 Person表示方法入参是引用传递,如果在方法中改变了入参的属性,会在方法外体现。

方法的继承

还是以上面的Person、Employee举例。

假设Person有个changeName方法,定义如下:

func (pptr *Person) changeName(name string) {

pptr.Name=name

}

假如在定义Employee时省略了Person类型属性的名称,则我们可以通过e直接打点调用changeName方法,示例1如下:

func main() {var e Employee= Employee{Person: Person{Name: "wanglaoji"}}e.changeName("lisi")
fmt.Printf("%+v\n", e)
}

假设Employee也有个changeName方法,则直接通过e打点调用changeName方法的话,调用的其实是Employee的changeName方法,而不是Person的changeName方法。示例2如下:

func (pptr *Person) changeName(name string) {

pptr.Name=name

}

func (eptr*Employee) changeName(name string) {
eptr.Name= name +name
}
func main() {var e Employee= Employee{Person: Person{Name: "wanglaoji"}}
fmt.Printf("%+v\n", e)
}

假如Person有个change方法,在change方法中调用了changeName方法,那么e调用change方法时,执行的是Employee的changeName方法呢,还是Person的changeName方法呢?示例3如下:

func (pptr *Person) changeName(name string) {

pptr.Name=name

}

func (pptr*Person) change(name string) {

pptr.changeName(name)

}

func (eptr*Employee) changeName(name string) {

eptr.Name= name +name

}

func main() {

var e Employee= Employee{Person: Person{Name: "wanglaoji"}}

e.change("lisi")

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

}

实测执行的是Person的changeName方法。

假如Employee也有个change方法,在change方法中调用了changeName方法,那么e调用change方法时,执行的是Employee的changeName方法呢,还是Person的changeName方法呢?示例4如下:

func (pptr *Person) changeName(name string) {

pptr.Name=name

}

func (pptr*Person) change(name string) {

pptr.changeName(name)

}

func (eptr*Employee) changeName(name string) {

eptr.Name= name +name

}

func (eptr*Employee) change(name string) {

eptr.changeName(name)

}

func main() {

var e Employee= Employee{Person: Person{Name: "wanglaoji"}}

e.change("lisi")

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

}

实测执行的是Employee的change方法和Employee的changeName方法。

  • 总结:
    在go中没有继承,不论是属性还是方法。都是太任性的省略搞的鬼。
    在go中没有静态方法的概念。

接口

定义格式:

type RedPacketService interface{
add(s string)
delete(s string)
update(s string)
query(s string) string
}

go中没有implements或者相同作用的关键字。要实现某个接口,不是在定义struct的时候显式声明要实现某个接口,而是采用duck typing的方式,即只要实现了接口的所有方法,就认为这个struct实现了这个接口,就可以向上转型。示例如下:

type Programmer interface{

helloWorld()

}
type JavaProgrammer struct {

}

func (jp*JavaProgrammer) helloWorld() {

fmt.Println("System.out.println(\"Hello World!\");")

}
type GoProgrammer struct {

}

func (gpGoProgrammer) helloWorld() {

fmt.Println("fmt.Println(\"Hello World!\")")

}

func main() {

var p Programmer= new(JavaProgrammer)

p.helloWorld()

p= new(GoProgrammer)

p.helloWorld()

}

Programmer接口只有一个helloWorld方法,JavaProgrammer实现了这个方法,所以JavaProgrammer实现了Programmer接口,同理,GoProgrammer也实现了Programmer接口。

这里有一点需要注意,JavaProgrammer是pointer receiver实现,GoProgrammer是value receiver实现,而实现接口的方法时,pointer receiver和value receiver是不一样的,参考go官方文档https://golang.org/doc/effective_go.html#pointers_vs_values

The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.
也就是说,value receiver方式实现的方法可以被实例和指向实例的指针调用,而pointer receiver方式实现的方法只能被指向实例的指针调用。

上例中,直接用JavaProgrammer{}生成一个实例,然后赋值给p会报编译错误,提示Cannot use ‘JavaProgrammer{}’ (type JavaProgrammer) as type Programmer. Type does not implement ‘Programmer’ as ‘helloWorld’ method has a pointer receiver.

假如再给Programmer接口添加一个hi方法,由于JavaProgrammer和GoProgrammer都没有实现这个方法,所以这两个struct都没有实现Programmer接口,所以把指向JavaProgrammer实例的指针或指向GoProgrammer实例的指针赋值给Programmer类型变量时会报编译错误。

接口最佳实践:

使用小的接口定义,接口中方法数不要太多;
较大的接口定义,由多个小接口定义组合而成;
只依赖于含必要功能的最小接口

type Reader interface{

read() string

}

type Writerinterface{

write()

}

type ReaderWriterinterface{

Reader

Writer

}

func getMessage(reader Reader) string {returnreader.read()

}

func getMessage2(readerWriter ReaderWriter) string {returnreaderWriter.read()

}

如上例,Reader接口提供read()方法,Writer接口提供write()方法,而ReaderWriter接口是由Reader接口和Writer接口组合起来的。函数getMessage内部只需调用Reader的read()方法,那么形参类型只需是Reader,而不应该是比Reader接口功能更多的ReaderWriter接口。

话说ReaderWriter接口和Reader接口是啥关系呢?是ReaderWriter接口继承了Reader接口,还是说Reader接口是ReaderWriter接口的成员变量?

空接口 interface{}

interface{}可以用作任意类型的形参,示例如下:

func PX(s interface{}) {if s0, ok := s.(int); ok {
fmt.Println("int=", s0)
}else if s0, ok :=s.(string); ok {
fmt.Println("string=", s0)
}else if s0, ok :=s.(bool); ok {
fmt.Println("bool=", s0)
}else{
fmt.Println("unknown type")
}

}
func main() {
PX("1")
PX(1)
PX(false)
PX(new(Programmer))
}

以上,s可以是任意类型变量。s.(int)有2个返回值,第一个返回值是s,第二个返回值是true或者false,如果s是int型变量,就是true,否则就是false。所以通过判断第二个返回值是否是true,就能判定s是否是int型变量。

以上写法还可以通过switch来简化判断,如下:

func PX(s interface{}) {switchs.(type) {case int:
fmt.Println("int=", s)casestring:
fmt.Println("string=", s)casebool:
fmt.Println("bool=", s)default:
fmt.Println("unknown type")

}

}

func main() {

PX("1")
PX(1)
PX(false)
PX(new(Programmer))
}

s.(type) 必须跟在switch后面,否则会报编译错误。

error

error是go自带的一个接口,只有一个Error()方法,没有入参,有一个string类型的出参。
go自带了很多实现了error接口的struct,如errorString、TimeoutError等。
errorString只有一个string型的属性,我们可以通过errors包的New(text <font string函数获取指向errorString实例的指针。示例如下:

func Check(i int) (int, error) {
if i < 0{
return -1, errors.New("negative")
} else{
return 1, nil
}
}
func main() {
i, err := Check(-1)
if err !=nil {
fmt.Println("something wrong")
} else{
fmt.Println("going on,i=", i)
}
}

error实例能否用==比较?

github.com/pkg/errors是一个比较好用的error相关的包。

Java Thead为线程与go的Goroutine

Java Thead为线程,线程是操作系统调度的最小单位。

1、线程间切换(不出让情况下):抢占式调度,随机执行。

2、实现:继承Thread类或者实现Runnable接口,Callable类似,或者通过线程池。

3、线程切换代价大

4、一般通过共享内存通信

Goroutine 为go并发执行的调度单位。

1、Goroutine间切换:业务要求切换(runtime.Gosched())、发生IO,等待chanel导致执行阻塞,否则单核goroutine是串行执行。

2、实现:function前加 ‘go’关键字

3、goroutine切换代价小

4、一般通过通信共享内存
goroutine可以说是golang实现的协程,不归操作系统管理。

Golang-切片slice与Java的ArrayList集合对比

Golang里面的切片,很像Java中的ArrayList,可以实现数组的动态扩缩容。

创建

// Java的ArrayList定义
List<String> list = new ArrayList<String>();
// Java数组定义
String[] arrayRefVar = new String[5];
// go这里使用make()第二个参数为切片的容量为5
slice := make([]string, 5)

Java中是通过new创建一个ArrayList,Go中是通过make来创建,这边就是语法上的不同。

赋值

在语法上趋近与数组的编写形式,而不像ArrayList的语法那么重量级。但是GO语言的数组[]号在类型的前面,这个是Java开发语言的同学所不适应的。

初始化赋值

// Java数组赋值
String[] str = {"Red","Blue"};
// GO切片赋值-->go的切片相当于数组的起始位置到偏移量的视图
str := []string{"Red","Blue"}

直接赋值

// Java
int[] b = new int[3];
b[0] = 100;
//GO
b := make([]int,3)
b[0] = 100

是不是跟Java的数组赋值差不多,GO语法中位置好多都是跟Java中相反的。

增加元素

java的ArrayList底层扩容是按照原数组的1.5倍,go的切片额外追加赋值的话,如果原数组容量不够,会在原来数组的基础上扩容2倍。

// Java
String[] strs = new String[4];
        strs[0] = "1";
        strs[1] = "2";
        strs[2] = "3";
        strs[3] = "4";
        List<String> list = new ArrayList<>(Arrays.asList(strs));
        List<String> newList = list.subList(1,3);
        System.out.println(newList.toString());
        list.add("5");//java的ArrayList底层扩容是按照原数组的1.5倍
        System.out.println(list.toString());

//GO
	strs := make([]string,4)
	strs[0] = "1";
	strs[1] = "2";
	strs[2] = "3";
	strs[3] = "4";
	newStrs := strs[1:3]
	t.Log(newStrs)
   // 额外追加赋值的话,如果原数组容量不够,会在原来数组的基础上扩容2倍
	strs = append(strs,"5")
	t.Log(strs)

这段代码的意思是创建一个动态数组,可以看到Go的语法要比Java要简洁的多。

遍历

GO语言for后面不用大括号,剩下的写法基本上一样。

// Java
    String[] strs = {"1","2","3","4"};
    List<String> list = new ArrayList<>(Arrays.asList(strs));
        for(String str : list){
            System.out.println(str);
        }
    System.out.println("---------------");
        for(int i=0;i<list.size();i++){                                            System.out.println("index="+i+",value="+list.get(i));
        }
// GO
	strs := []string{"1","2","3","4"}
	for index, value := range strs{
		fmt.Printf( "index=%d, value=%s\n", index, value)
	}
	for index:=0;index<len(strs);index++{
		fmt.Printf( "index=%d, value=%s\n", index, strs[index])
	}

二、 go语言与java的语法对比

1.编译与运行

##java用jvm做到程序与操作系统的隔离. 一次性编译生成的class文件,处处可以运行
## 简单来说 windows上编译的出class文件,拿到安装了jvm的linux上可直接可以执行
javac Hello.java
java Hello
## go 在不同的操作系统build出对应的可执行文件 
go build hello.go
## 当然直接编译+执行亦可,慢一些
go run hello.go

2.main方法与测试类

// java 源码 "一等公民"是 类,源码文件 通过类来区分关联
public class Hello {  // 一个 类下只有一个启动方法
    public static void main(String[] args) {
        System.out.println("hello java");  // jdk提供的内部类不需要导入
    }  
}
// jdk本身无测试类,需要引入第三方依赖包 junit4
public class Test {
    @org.junit.Test  // 外部依赖的导入
    public void test() {
        System.out.println("this is test");
    }
}
``

?```go
// 1. go "一等公民" 是函数, 源码 通过包名 区分关联
// 一个文件夹下,可以有多个源码文件,但必须 统一 包名,不过 包名 ≠ 文件夹名
package main  // 定义包名
import "fmt"  // 导入内部 "包"
func main() { // 每个包 下只允许一个 启动方法
	fmt.Println("hello world")
}
// 2. 自带内部包 有test 功能
// 源码文件 后缀 _test 标注
import (
	"fmt" 
	"testing"  // 导入内部测试包 
)
func TestFirstTry(t *testing.T) { // 函数 Test 开头
	fmt.Println("dadayu try")
}

3.变量与常量

// 每一个变量都有属于自己的类型
public static final int MONDAY = 1; // 常量
public static final int TUESDAY = 2;
public static void main(String[] args) {
    // 案例: 交换 数值型 a= 1 b=2 的值
    int a = 1;
    int b = 2;
    int tmp = a;  // 中间变量
    a = b;
    b = tmp;
}

go语言

const (
	Monday = iota + 1  // 常量可自增定义
	Tuesday
	Wednesday 
)   
func TestChange(t *testing.T) {
	// 1. var 声明
	//var a int = 1
	//var b int = 2
	// 2. var 类型推断
	//var (
	//	a = 1
	//	b = 2
	//)
	// 3. 类型自动推断
    a := 1
	b := 2
	a,b = b,a   // 值可以直接交换
	t.Log(a,b)
}

4. 类型 type

func TestImplicit(t *testing.T) {
	var a int= 1
	var b int64
	// 1. 不支持 隐式 类型转换 即使是同一种 变量
	// 必须使用显式类型转换
	b = int64(a)
    // 2.go 可以 获得指针
    aPrt := &a  // & 取址符
    var str string // 3. string 是值类型 , 初始化是 ""
	t.Log(str == "")  // true
}

5.条件判断 condition

  • java代码
// java 多条件判断
 if (n % 2 == 0) {
     return "Even";
 } else if (n % 2 == 1) {
     return "Odd";
 } else {
     return "Unkonw";
 }
  • go语言
//1. go的 switch 也可以实现 if -else 
func SwitchCaseCondition(i int) (string, bool) {
	switch {
	case i % 2 == 0: // 留意 java的 switch 需要break嗷!
		return "Even", true // go的函数 支持多返回值 特色之一
	case i % 2 == 1:
		return "Odd", true
	default:
		return "Unkonw", false
	}
}
func TestSwitch(t *testing.T) {
    //2. go的 if 可以边变量声明, 边进行判断
	if str,flag := SwitchCaseCondition(5); flag {
		t.Log(str)
	}
}

6.循环语句 loop

  • go代码
// go 关键字 很少 循环只有 for
func TestWhileLoop(t *testing.T) {
	n := 0
    // 1. go 实现 while 
	// 等同于 while(n < 5)  不需要 () , 关键字 只有for
	for n <5 {
		t.Log(n)
		n++
	}
    // 2. 常规的 for循环   // 去掉 () 没啥 区别
    	for i:= 0; i < 5; i++ {
		t.Log(i)
	}
}

7.数组 array

  • java代码
// 初始化 定长的数组, 默认为0
int[] arr= new int[5];
arr[1] = 2;
int[] arr1 = new int[]{1,2,3,4};  // 初始化 也可赋值
for (int i : arr1) { // 遍历 数组
    System.out.println(i);
}
  • Go语言
var arr [3]int //1. 初始化, 默认为 0
arr[2] = 3  
arr1 := [3] int{1,2,3} // 声明 并初始化
arr2 := [...] int {1,2,3} // 自动长度推断
t.Log(arr1 == arr2) //2. 结果 : true 数组是值类型
for i, v := range arr1 {
    t.Log(i,v)  // 3.注意 变量声明后必须使用, 如果不行用咋办
}
for _, v := range arr1 {
    t.Log(v) // 严格 编程约束 下, 可以使用 占位符 _ 来代替
}
// 4. 数组的切分
// a[开始索引(包含), 结束索引(不包含)]
arr3 := arr1[1,len(arr1)]  // 得到索引1,往后的值 即 2,3

8. 切片 slice

go的引用类型之一,简单说 切片就是可变长的数组引用 , 数组通过len函数可以得到长度. 而切片除了真实的长度 ,还有 用cap函数 获得其容量, 我们会初始化一个容量, 如果数据真实长度超过这个容量.切片就会发生扩容,这时候会发生值copy. 这里联想一下java, 这不就是 ArrayList

  • java代码:
ArrayList<Integer> list = new ArrayList<>(8);// 初始化 容量为 8
list.add(1);	
System.out.println(list.size()); // 数据真实长度
  • go语言:
var sli []int // 初始化
sli = append(sli, 1) // 添加元素
t.Log(len(sli), cap(sli))  // 1,1
// 初始化 并赋值
sli1 := [] int{1,2,3,4}
t.Log(len(sli1), cap(sli1)) //4 , 4
// 初始化 容量大小
sli2 := make([]int, 3, 5)
//make 是专门用来创建 slice、map、channel 的值的。
// 它返回的是被创建的值,并且立即可用。
t.Log(sli2 ,len(sli2), cap(sli2)) // [0 0 0] 3 5
// 如何扩容
sli3 := [] int{1,2,3,4}
fmt.Printf("%p\n",sli3) // 0xc00009c140
sli3 = append(sli3, 5)
fmt.Printf("%p\n",sli3) //0xc0000ba240  引用的内存地址改变 copy了新的引用
t.Log(sli3 ,len(sli3), cap(sli3)) // 与 ArrayList的扩容机制 完全相同

9. go的map与java的

map亦是go的基本引用类型之一,go比较有特色的一点是,java里面很多基本的集合(链表,堆),在go里面都是作为自定义类型,"自举"实现的(即用 Go 语言编写程序来实现 Go 语言自身), 可以直接阅读go的源码包学习,里面甚至有测试的案例:
java代码

HashMap<Integer, Integer> map = new HashMap<>(); //约定类型
map.put(1,1); // 添加
boolean b = map.containsKey(3); // 判断 有没有
map.remove(1); // 移除
// 遍历
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
 System.out.println(entry.getKey() + entry.getValue());
}
// 初始化 并赋值
map2 := map[int]int{1: 1, 2: 2, 3: 9}
map2[2] = 4 // 修改
map2[4] = 16 // 添加
t.Log(map2,len(map2)) //  map[1:1 2:4 3:9 4:16] 4
map3 := make(map[int]int,10)
t.Log(map3,len(map3)) // map[] 0
// 遍历
for k, v := range map2 {
    t.Log(k,v)
}

// 案例1 :map 实现 set
mySet := map[int] bool{}
mySet[1] = true
n := 3
// 判断值是否存在
if mySet[n] {
    t.Logf("%d is exiting", n)
}else {
    t.Logf("%d is not exiting", n) // 3 is not exiting
}
// 案例2 :map的value 可以放函数 , java 中一般放对象
// 很容易的实现了 工厂模式
funcMap := map[int] func(op int) int{}
funcMap[1] = func(op int) int { return op}
funcMap[2] = func(op int) int { return op * op}
funcMap[3] = func(op int) int { return op * op * op}
t.Log(funcMap[1](2),funcMap[2](2),funcMap[3](2)) //2 4 8

10. go通道 channel

chan基本的引用类型之一,Go 语言最有特色的数据类型,通道(channel)完全可以与 goroutine( 协程)并驾齐驱,共同代表 Go 语言独有的并发编程模式和编程哲学。
channe类型的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类型。
一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符**<-**。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。

// 这里 简单演示一下 channel ,并发时, channel 大有用途
func TestChan(t *testing.T) {
	ch1 := make(chan int, 1)  // buffer chan ,队列的大小
	ch1 <- 1 // 输入
	elem1 := <-ch1 // 输出
	fmt.Printf("The first element r: %v\n", elem1)
	delay(ch1)  
	fmt.Println("put elem to channel")
	ch1 <- 2 
	time.Sleep(time.Second * 1) //守护线程
}
func delay(ch chan int) {
	go func() {   // 开启协程
		fmt.Printf("receive elem from chanel %d", <-ch) // 阻塞式等待
	}()
}
###################结果打印#####################
The first element r: 1
put elem to channel
receive elem from chanel 2

11. 函数 function

在 Go 语言中,函数可是一等的(first-class)公民 ,函数类型也是一等的数据类型.相较于java , 函数基本的语法差异在于 可以多返回值. 更关键的是 天然实现 了java中的 函数式编程, 1.接受其他的函数作为参数传入 2.把其他的函数作为结果返回.
java代码:

// 案例1 声明函数类型
static Predicate<Integer> judgeEven = e -> e % 2 == 0;  //函数式编程 搭配 lambda更香..
static Consumer<Integer> printlnInt = System.out::println;
// 案例 2 计算函数执行的耗时的切面通用方法  装饰者模式, 不得不说, 学习成本 有点高(手动笑哭)
static Function<Integer, Integer> timeSpent(Function<Integer, Integer> inner) {
    return (Function<Integer, Integer>) integer -> {
        Instant first = Instant.now();
        Integer ret = inner.apply(integer); // 执行传入的方法
        Instant second = Instant.now();
        System.out.println("time spent:"+Duration.between(first, second).toMillis());
        return ret;
    };
}
public static int simpleFunc(int n)  {
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (Exception e) {
    }
    return n;
}
public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
    list.stream().filter(judgeEven).forEach(printlnInt); // 2,4,6
    Function<Integer, Integer> func = timeSpent(funcInterface::simpleFunc);
    func.apply(10); //  time spent:1000
}

go代码:

// 案例一 go 实现 装饰者模式
// 函数类型 (接口) 自定义函数的签名
type myFunc func(op int) int
func timeSpent(inner myFunc) myFunc { //
	return func(n int) int{
		start := time.Now()
		ret := inner(n)
		// 计算日期 since 从什么时候开始
		fmt.Println("time spent:", time.Since(start).Seconds())
		return ret
	}
}
func slowFun(op int) int{
	time.Sleep(time.Second*1)
	return op
}
 // 案例2 延迟执行 方法
func Clear()  {
	fmt.Println("Clear resources.")
}
func TestFn(t *testing.T) {
	// 直接 拿到增强后的方法
	tsSF := timeSpent(slowFun)
	t.Log(tsSF(10))  
    defer Clear() // 类似于 java的 finally
	fmt.Println("Start")
	// 异常报错, 也会有延迟执行
	panic("err")  
}
###############结果展示##############
Start
Clear resources.

12. go面向对象 (封装)

Go 严格上不能算是一个面向对象的语言,但是通过go 基本可以模拟出类似java的 面向对象的效果.

type Employee struct {  // struct 结构体封装了 java的属性
	Id string
	Name string
	Age int
}
// 给 (e Employee) 结构体的指针 绑定行为  类似java的行为
func (e *Employee) String() string{
	return fmt.Sprintf("ID:%s/Name:%s/Age:%d", e.Id, e.Name, e.Age)
}
func TestCreateEmployeeObj(t *testing.T) {
    e := Employee{"0", "Bob", 20}
	e1 := Employee{Name: "Mike", Age: 30}
	e2 := &Employee{Name: "Mike", Age: 30} // 返回对象的 指针
	e3 := new(Employee)  // 返回 对象的 指针
	e3.Id = "2"
	e3.Name = "Rose"
	e3.Age = 22
	t.Log(e1)  // { Mike 30}
	t.Log(e2)  // ID:/Name:Mike/Age:30
	// %T 代表类型  encap_test 是包名
	t.Logf("e is %T", e)  //e is encap_test.Employee 
	t.Logf("e2 is %T",e2)  //e2 is *encap_test.Employee
}

13. go面向对象 (继承)

java中子类继承父类属性与行为的,通过实现子类的方法,重写父类方法, 实现了LSP 里氏替换原则.往往作为策略模式在代码中体现.Go 无法做到继承. 只能做到复用 父类方法. 再Go的语言哲学里. 组合 > 继承. 其实这也是 java的spring 框架中 依赖注入的思想.

type Pet struct { // 父结构体
}
func (p *Pet)Speak() { // 行为
	fmt.Print("...")
}
func (p *Pet)SpeakTo(host string) { // 主流程
	p.Speak()
	fmt.Println("", host)
}
type Dog struct { 
	Pet // 匿名嵌套类型 , 类似继承的作用, 使用了父类的 方法
}
func (d *Dog) Speak()  { //重写 父类方法
	fmt.Print("wang!!")
}
func TestDog(t *testing.T) {
	// 不支持隐式类型转换
	dog := new(Dog)
	dog.Speak() // 可以覆盖
	dog.SpeakTo("chao") // 但是无法重写父结构体的方法
}
#############执行结果##############
wang!!
... chao

14. go面向对象 (多态)

// 类型别名
type Code string
type Programmer interface {
	WriterHelloWorld() Code
}
// 无显式的  implement 关键字
type GoProgrammer struct { // 第一个接口实现
}
func (g *GoProgrammer)WriterHelloWorld() Code {
	return "fmt.Println(\"Hello World\")"
}
type JavaProgrammer struct { // 第二个接口实现
}
func (j *JavaProgrammer) WriterHelloWorld() Code {
	return "system.out.println(\"Hello World\")"
}
func writeFirstProgram(p Programmer)  {  // 将接口传入方法
	fmt.Printf("%T, %v\n", p, p.WriterHelloWorld()) // 接口的 多态
}
func TestPolymorphic(t *testing.T) {
	//var p Programmer = new(JavaProgrammer)
	// 接口 参数必须有指针 引用  & 取址符
	p := &JavaProgrammer{}
	writeFirstProgram(p) // 
	goPro := new(GoProgrammer)
	writeFirstProgram(goPro)  
}
###################执行结果#####################
*polym__test.JavaProgrammer, system.out.println("Hello World")
*polym__test.GoProgrammer, fmt.Println("Hello World")

15. go并发 concurrent

Java的并发线程模型

线程- Thread 是比 进程 - Progress 更轻量的调度单位. 众所周知, 操作系统会把内存分为内核空间和用户空间, 内核空间的指令代码具备直接调度计算机底层资源的能力.用户空间的代码没有访问计算底层资源的能力,需要通过系统调用等方式切换为内核态来实现对计算机底层资源的申请和调度. 线程作为操作系统能够调度的最小单位,也分为用户线程和内核线程. 常用的java的线程模型属于一对一线程模型. 下图中反映到Java中, Progress是jvm虚拟机. LWP就是程序开启的Thread, 1:1 对应上内核线程,最后通过操作系统的线程调度器, 操作底层资源.
在这里插入图片描述
进程内每创建一个新的线程都会调用操作系统的线程库在内核创建一个新的内核线程对应,线程的管理和调度由操作系统负责,这将导致每次线程切换上下文时都会从用户态切换到内核态,会有不小的资源消耗。好处是多线程能够充分利用 CPU 的多核并行计算能力,因为每个线程可以独立被操作系统调度分配到 CPU 上执行指令,同时某个线程的阻塞并不会影响到进程内其他线程工作的执行。以上就是 java并发的特色. 我们用代码测试一下.

//java 案例 1 开始多线程来打印,中间线程休眠,让出cpu的执行资源
class PrintIntTask implements Runnable{   // 任务类的封装
    int num;   // 传入的 参数
    CountDownLatch cnt; // 并发协同 线程计数器
    public PrintIntTask(int num, CountDownLatch cnt) {
        this.num = num;  
        this.cnt = cnt;
    }
    @Override
    public void run() {
        TimeUnit.SECONDS.sleep(1);  // 休眠 1s
        System.out.println(num);    // 打印
        cnt.countDown();			// 计数锁 -1 
    }
}
public static void main(String[] args) throws InterruptedException {
    Instant first = Instant.now();
    int num = 10000;
    CountDownLatch countDownLatch = new CountDownLatch(num);// 计数锁
    for (int i = 0; i < num; i++) {   
        new Thread(new PrintIntTask(i,countDownLatch)).start(); // 任务分配,开启线程
    }
    countDownLatch.await(); //守护线程 ,子线程改造时,切记阻塞住 主线程
    Instant second = Instant.now();
    System.out.println("spent time:" + Duration.between(first, second).toMillis());
}

java线程池的作用

############# 上一页程序 测试结果 ###################
n = 1000,   spent time:1106 ms    //  资源竞争 不明显
n = 10000,  spent time:5970	ms    //  资源竞争 明显

虽然每个线程之间是独立的,但是处于就绪状态的线程,需要被cpu调度才能进入运行态**, 当线程休眠时,会进入等待状态,让出cpu资源,执行其他线程**. 而开辟的线程数上升后,竞争也愈发明显.同时开启大量的线程,对于系统的内存资源也会有很大的负担,在Jvm内存模型中,线程主要有

1.程序计数器

线程数超过CPU内核数量时,线程之间就要根据时间片轮询抢夺CPU时间资源,线程等待让出cpu资源时,它就需要记录正在执行字节码指令的地址.

2.虚拟机栈

每个方法执行时,会开辟一个栈帧,存储局部变量表,操作数栈等.调用方法时,生成一个栈帧,压入栈中,退出方法时,将栈帧弹出栈. 通过配置jvm参数 -Xss可以设置栈的大小,一般为1M.这就是递归方法过多后StackOverFlowError 的原因.

所以在java 编程中,需要开启大量的线程时一定要控制, 这就是常说的 线程池, 配置同时执行的核心线程数, 多余的任务在内部队列中排队执行.

 //IO密集型 一般是 cpu 核心数 * 2 , 计算密集型 一般是 cpu核心数
int processors = Runtime.getRuntime().availableProcessors() * 2 ;
// 初始化线程池,阿里巴巴手册推荐手动new,设置相应的 拒绝策略,任务队列的参数 ,巴拉巴拉 之类的东西
Execu torService fixedThreadPool = Executors.newFixedThreadPool(processors);
for (int i = 0; i < num; i++) {
    fixedThreadPool.submit(new PrintIntTask(i));// 将任务塞入线程池,线程自己调度开启
}
// 线程池 会自动守护线程 不会中断子线程的运行

Go的并发线程模型

Go在一对一线程模型的基础上,做了一些改进,提出了MPG模型 ,让线程之间也可以灵活的调度。
machine,一个 machine 对应一个内核线程,相当于内核线程在 Golang 进程中的映射。
processor,一个 prcessor 表示执行 Go 代码片段的所必需的上下文环境,理解为代码逻辑的处理器。
goroutine,是对 Golang 中代码片段的封装,其实是一种轻量级的用户线程,我们叫协程。
下左图,每个M都会和一个内核线程绑定,M与P 也是一对一关系,而P与G时一对多的关系.M在生命周期内跟内核线程是至死不渝的.而 M 与 P ,P 与 G.那是自由恋爱.

下右图,M与P的组合为G 提供了运行环境, 可执行的G的挂载在P下,等待调度与执行为GO。 这里不得不提 java的ForkJoinPool的工作窃取算法(感兴趣的2021.5.4 朋友圈有篇). 这里把G看做任务,当P空闲时,首先去全局的执行队列中获取一些G。如果没有则去"窃取"其他P最后的G,保证每一个P都不摸鱼。
在这里插入图片描述

//go 案例 1 开始多协程来打印,中间协程休眠,让出cpu的执行资源
func TestGroutine(t *testing.T) {
	start := time.Now()
	num := 10000
	wg := sync.WaitGroup{}
    wg.Add(num)
	for i := 0; i < num; i++ {
		go func(i int) {  // 协程  可以直接传参进去...
			time.Sleep(time.Second * 1)
			fmt.Println(i)
			wg.Done()
		}(i)
	}
	wg.Wait()
	t.Logf("spent time :%d",time.Since(start).Milliseconds()) //  10000个协程
}
#################测试结果######################
n = 1000,   spent time:1005 ms    //  java spent time:1106 ms
n = 10000,  spent time:1262 ms    //  java spent time:5970 ms

协程其实就是更轻量的线程,go将cpu的调度转为用户空间协程的调度,避免了内核态和用户态的切换导致的成本,通过队列的形式挂载协程, 这些协程共享一个P获得cpu资源.这样我们可以大量的开启协程,而不用太担心协程之间的竞争压力,同时G的栈空间只有2k,无压力创建出大量的实例,高效的利用了系统资源.

资源锁

单纯的并发还蛮简单,但是事情总不是一帆风顺. 并发在做任务时,不可避免的会出现共享资源,当多个线程同时操作同一资源时,就会出现并发安全问题。 我们使用资源锁,拿到锁的线程才可以操作资源, 拿不到锁的线程,等待锁的释放,再去抢锁。
java代码:

public class Count {   // 案例1 : 1000 个并发操作计数  9:1 读写比
 	 // 可重入锁
    final  ReentrantLock lock = new ReentrantLock(); //参数true表示公平锁,性能会低
    CountDownLatch cdl; 
    int count = 0;
    public Count(CountDownLatch cdl) { this.cdl = cdl;}
    public void read(){
        lock.lock();
        try {
            Thread.sleep(1);  // 模拟查询消耗的时间
            System.out.println("查询现在计数为:"+count);
        } finally {
            lock.unlock(); // 一定要记得解锁嗷
            cdl.countDown();
        }
    }
    public void count(){
        lock.lock();
        try {
            count++;
            Thread.sleep(5); // 模拟修改消耗的时间
            System.out.println("计数后现在计数为:"+count);
        }  finally {
            lock.unlock(); // 一定要记得解锁嗷
            cdl.countDown();
        }
    }
}

int num = 1000;
CountDownLatch cdl = new CountDownLatch(num);
Instant start = Instant.now();
//        Count count = new Count(cdl);
Count1 count = new Count1(cdl);
for (int i = 0; i < num; i++) {
    if (i % 10 == 0){
        new Thread(count::count).start();
    } else {
        new Thread(count::read).start();
    }
}
cdl.await();
Instant end = Instant.now();
System.out.println("spent time:" + Duration.between(start, end).toMillis());

####################测试结果#####################
spent time  :  1624

锁的优化

加锁之后,并发的性能明显下降,所以使用锁时一定要慎重, 从优化的来看有两个维度:

★其一,缩小锁的粒度. 在分布式事务中,如果多个系统事务保证强一致性,并发能力必然很差.,若把一个大事务,分割几个独立的小事务, 只要能保证最终一致性,就能大大提示并发能力.

★其二,读写锁,第一个案例里面,提到一个读写比的概念,这里联想一下数据库的隔离级别,读,写操作都互斥时,不就是性能最低的串行化SERIALIZABLE.把读和写 设计成两钟锁,存在写锁时,才互斥,只有读锁时,不互斥,再读写比较大场景下, 并发性能得到提升.

final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); // 案例2 读写锁
CountDownLatch cdl;
int count = 0;
public Count1(CountDownLatch cdl) { this.cdl = cdl;}
public void read(){
    lock.readLock().lock();  // 读锁 加锁
    try {
        Thread.sleep(1);
        System.out.println("查询现在计数为:"+count);
    } finally {
        lock.readLock().unlock(); // 读锁解锁
        cdl.countDown();
    }
}
public void count(){
    lock.writeLock().lock();  // 写锁 加锁
    try {
        count++;
        Thread.sleep(5);
        System.out.println("计数后现在计数为:"+count);
    } finally {
        lock.writeLock().unlock(); //写锁 解锁
        cdl.countDown();
    }
}
################测试结果#################
spent time   :   699    // 并发的性能得到明显提升

go实现读写锁

num := 1000
	wg := sync.WaitGroup{}  
	wg.Add(num)
	mutex := sync.RWMutex{}
	count := 0
	for i := 0; i < num; i++ {
		if i % 10 ==0 {
			go func() {
				defer func() { // 延迟执行方法, 类似于finally
					mutex.Unlock() 
				}()
				mutex.Lock()  // 写锁
				count = count + 1
				fmt.Printf("write num:%d\n",count)
				wg.Done()
			}()
		}else {
			go func() {
				defer func() {
					mutex.RUnlock() // 读锁
				}()
				mutex.RLock() // 读锁
				fmt.Printf("read num:%d\n",count)
				wg.Done()
			}()
		}
	}
	wg.Wait()

go的CSP并发通信模型

go在并发上的突破不光在线程模型上, 传统的并发通信模型,是以共享内存的方式,通过原子类和管程进行同步控制,比如上述的锁,有锁就有竞争,go 还提供了另一种方案, 支持协程之间以消息传递(Message-Passing)的方式通信, Go有句格言, “不要以共享内存方式通信,要以通道方式共享内存”.

通道即channel. 有点类似消息队列。.分非缓冲通道和 缓冲通道, 前者协程之间建立通信后,同步收发消息, 后者初始化通道容量, 异步收发消息,通道满了或空了,阻塞等待.。
在这里插入图片描述

num := 1000
wg := sync.WaitGroup{}
wg.Add(num)
start := time.Now()
ch := make(chan int, 1) // 容量为1的缓冲通道 数据类型为 int
ch <- 0 // 放入通道初始值
for i := 0; i < num; i++ {
    if i % 10 ==0 {
        go func() {
            count := <- ch  // 从通道拿数据
            time.Sleep(time.Millisecond * 5)
            fmt.Printf("write num:%d\n",count)
            ch <- count+1 // +1 后再放入通道
            wg.Done()
        }()
    }else {
        go func() {
            count := <- ch // 从通道拿数据
            ch <- count   // 获得数据之后,直接放回即可
            time.Sleep(time.Millisecond * 1)
            fmt.Printf("read num:%d\n",count)
            wg.Done()
        }()
    }
}
wg.Wait()
t.Logf("spent time :%d",time.Since(start).Milliseconds()) //spent time :550

线程协同 (任务并行)

并发中的关键场景之一.我们调用两个耗时却又毫无关联的两个组件时,不妨试试任务并行,就是烧水煮茶, 时间统筹,同时执行.
java代码:

Instant start = Instant.now(); // 案例 1  java异步编程类 FutureTask
FutureTask<String> futureTask = new FutureTask<String>(()->{
    TimeUnit.SECONDS.sleep(1); // FutureTask java特色的异步编程任务类
    return "haha";
});
new Thread(futureTask).start(); // 异步执行
TimeUnit.SECONDS.sleep(1);
String s = futureTask.get(2, TimeUnit.SECONDS); //阻塞等待, 且限定超时时间
Instant end = Instant.now();
System.out.println("spend time :" + Duration.between(start, end).toMillis() + "ms");
#####################执行结果######################
spend time :1050ms // 执行时间 取决于 最长的时间

go代码:

//go 简单的异步有案例,这里加上  通道传递 + 多路选择 (有点 nio 的味道) 
select {   // 多路选择
    case ret:= <- syncTask(): // 等待通道返回
    t.Log(ret)
    case <-time.After(time.Second *3):   // 超时 返回
    t.Error("timeout")
}
func syncTask() chan string{
	ret := make(chan string,1)
	go func() {
		// 异步往通道写
		time.Sleep(time.Second * 1)
		ret <- "dada"
	}()
	return ret
}

拓展:java的FutureTask之(任务只执行一次)

// 案例2 多个线程创建连接时,只有一个线程可以创建,并建立连接的缓存,优先从缓存连接中拿
// 在 netty中 ,也是使用这种方式,保存建立好的channel,优化资源利用
private ConcurrentHashMap<String,FutureTask<Connection>>connectionPool = new ConcurrentHashMap<String, FutureTask<Connection>>(); 

public Connection getConnection(String key) throws Exception{
    FutureTask<Connection>connectionTask=connectionPool.get(key);
    if(connectionTask!=null){
        return connectionTask.get();
    }
    else{
       FutureTask<Connection> newTask = new FutureTask<Connection>(this::createConnection);
        connectionTask = connectionPool.putIfAbsent(key, newTask);//保证一个线程创建
        if(connectionTask==null){
            connectionTask = newTask;
            connectionTask.run();0
        }
        return connectionTask.get();// 本来要创建连接的线程,转为阻塞等待
    }
}
//创建Connection
private Connection createConnection(){
    return null;
}

参考阅读多篇博客:
原文链接:https://www.cnblogs.com/shenguanpu/archive/2013/05/05/3060616.html
原文链接:https://blog.csdn.net/weixin_45469930/article/details/122434441

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-06-14 22:21:09  更:2022-06-14 22:23:42 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 18:47:05-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码