TypeScript trifft Go: Das 10x TypeScript verstehen
Ethan Miller
Product Engineer · Leapcell

Umfassende Untersuchung der Migration von TypeScript zu Go: Entscheidungen, Vorteile und Zukunftsaussichten
I. Projekthintergrund und Ursprung
(I) Der Ursprung des Projektnamens
Der Codename des neuen TypeScript-Migrationsprojekts ist Corsa. Die alte Codebasis, Strata, war einst der ursprüngliche Codename von TypeScript, das Ende 2010 oder Anfang 2011 in der internen Entwicklungsphase begann. Das ursprüngliche Team bestand aus Steve Lucco, Anders Hejlsberg und Luke. Steve schrieb den ursprünglichen Prototyp-Compiler, indem er den Scanner und Parser aus der JavaScript-Engine von Internet Explorer extrahierte und modifizierte. Es war eine C#-Codebasis, die für den Proof of Concept verwendet wurde.
(II) Leistungsprobleme als Antrieb für die Änderung
In der ECMAScript-Community ist es ein Trend, stark toolabhängige Projekte zu nativem Code zu migrieren, wie z. B. esbuild und swc. TypeScript steht vor Leistungs- und Skalierbarkeitsproblemen. Da das Projekt immer weiter wächst, übt der Compiler mehr Druck auf die V8- und JavaScript-Engines aus. Die Startzeit verlängert sich durch das Hinzufügen neuer Funktionen. Frühere Optimierungen konnten nur eine Verbesserung von 5 % - 10 % bringen, womit die Grenze der Leistungsoptimierung erreicht wurde.
II. Gründe für die Wahl der Go-Sprache
(I) Vergleich mit der Rust-Sprache
- Speicherverwaltung und Kompatibilität: Die bestehende TypeScript-Codebasis geht von der Existenz einer automatischen Speicherbereinigung aus. Die Speicherverwaltung in Rust ist jedoch nicht automatisch. Der Borrow-Checker hat strenge Einschränkungen hinsichtlich des Besitzes von Datenstrukturen und verbietet zyklische Datenstrukturen. Die Datenstrukturen von TypeScript, wie z. B. der Abstract Syntax Tree (AST), verwenden in großem Umfang zirkuläre Referenzen. Die Migration zu Rust erfordert eine Neugestaltung der Datenstrukturen, was eine große Schwierigkeit darstellt. Daher ist Rust im Grunde ausgeschlossen.
- Entwicklererfahrung und Lernkosten: Es ist einfacher, von JavaScript zu Go zu wechseln als zu Rust. In einigen Aspekten ähnelt Go-Code JavaScript-Code. Beim Umgang mit komplexen oder rekursiven Strukturen in Rust ist es schwieriger, den Evolutionsprozess vom TypeScript-Code zu verstehen. Aus der Perspektive der Humanressourcen hat die Wahl von Go mehr Vorteile.
(II) Vergleich mit der C#-Sprache
- Sprachdesign-Orientierung: Go ist eine Sprache, die nativem Code mehr Priorität einräumt. Sie verfügt über die Funktion der automatischen Speicherbereinigung und ist ausdrucksstärker in Bezug auf Datenstrukturlayout und Inline-Strukturen. C# ist in gewisser Weise Bytecode-orientiert. Obwohl es eine Ahead-of-Time-Kompilierung gibt, ist sie nicht auf allen Plattformen verfügbar, und sie wurde von Anfang an nicht mit dem Ziel der nativen Leistungsoptimierung entwickelt.
- Unterschiede in den Programmierparadigmen: Die JavaScript-Codebasis von TypeScript verwendet einen stark funktionalen Programmierstil, und der Kerncompiler verwendet selten Klassen. Go konzentriert sich auch auf Funktionen und Datenstrukturen. Im Gegensatz dazu ist C# hauptsächlich objektorientierte Programmierung (OOP). Die Migration zu C# erfordert einen Wechsel des Programmierparadigmas, was die Migrationsreibung erhöht.
(III) Die vorteilhafte Passform der Go-Sprache
Die Go-Sprache kann auf allen gängigen Plattformen hervorragend optimierten nativen Code bereitstellen. Sie verfügt über eine gute Ausdrucksfähigkeit für Datenstrukturen, die zyklische Datenstrukturen und Inline-Datenstrukturen ermöglicht. Sie verfügt über die Fähigkeiten der automatischen Speicherbereinigung und des gleichzeitigen Zugriffs auf gemeinsam genutzten Speicher sowie über eine gute Toolchain und exzellenten Support von VS Code und anderen Tools. Sie erfüllt die vielfältigen Anforderungen der TypeScript-Migration und sticht unter vielen Sprachen hervor.
III. Herausforderungen des Projekts und Lösungen
(I) Der Kompromiss, auf Bootstrapping zu verzichten
Eine Bootstrapping-Sprache ist eine Sprache, die in sich selbst geschrieben ist. TypeScript war zuvor eine Bootstrapping-Sprache. Es gibt Bedenken, auf das Bootstrapping nach der Migration zu Go zu verzichten, aber für eine 10-fache Leistungssteigerung entscheidet sich das Team dennoch für die Migration. Einige Teile, die in JavaScript geschrieben wurden, werden jedoch beibehalten, wie z. B. der Language-Service-Teil. Das Team untersucht Lösungen für den Aufbau einer API zwischen dem nativen Teil (Go) und den Konsumenten anderer Sprachen.
(II) Bemühungen zur Gewährleistung der Kompatibilität
TypeScript hat keine offizielle Spezifikation, und die Referenzimplementierung ähnelt einer Spezifikation. Bei der Migration zu Go ist es notwendig, die semantische Konsistenz aufrechtzuerhalten. Das Ziel des Teams ist eine Kompatibilität von 99,99 % und die Hoffnung, für dieselbe Codebasis genau die gleichen Fehler zu erzeugen. Derzeit kann der Open-Source-Compiler den gesamten Visual Studio Code kompilieren und überprüfen, und er kann 20.000 Konformitätstests ohne Absturz ausführen. Das Team analysiert die Fehler-Baseline und beseitigt Unterschiede, mit dem Ziel, ein Plug-and-Play-Ersatz für den alten Compiler zu werden.
(III) Das Problem der Determiniertheit bei der Typsortierung
Der alte Compiler verwendete eine einfache nicht-deterministische Typsortierungsmethode, die in einer Single-Thread-Umgebung deterministisch, aber in einer Multi-Thread-Concurrent-Umgebung nicht-deterministisch war. Die neue Codebasis muss eine deterministische Typsortierung einführen, was in einigen Fällen zu unterschiedlichen Typreihenfolgen gegenüber dem alten Compiler führt. Insbesondere die Reihenfolge der Union-Typen ist in einigen Szenarien wichtig, und das Team arbeitet an der Lösung dieser Probleme.
(IV) Das Dilemma des API-Designs
Nahezu die gesamte interne Struktur des Compilers in der alten Codebasis wurde als API offengelegt. Die neue Codebasis muss die API neu gestalten und die Effizienz der API während der Interprozesskommunikation berücksichtigen. Derzeit untersucht das Team, wie eine versionierbare und moderne API für die neue Codebasis bereitgestellt werden kann.
IV. Die Anwendung und Vorteile der Parallelverarbeitung im Projekt
(I) Die funktionale Programmiergrundlage des Compilers erleichtert die Parallelverarbeitung
Der TypeScript-Compiler verwendete ursprünglich ein funktionales Programmiermodell und verwendete in großem Umfang Unveränderlichkeit, um eine sichere gemeinsame Nutzung zu gewährleisten. Beispielsweise wird der AST nach dem Scannen, Parsen und Binden grundsätzlich als unveränderlich betrachtet. Mehrere Typchecker können denselben AST gleichzeitig verarbeiten, was eine gute Grundlage für die parallele Verarbeitung bietet, auch wenn JavaScript selbst keinen Parallelverarbeitungsmechanismus für gemeinsam genutzten Speicher hat.
(II) Die Implementierung der Parallelverarbeitung in der Parsing-Phase
Die Parsing-Aufgabe eignet sich sehr gut für die Parallelisierung. Die Parsing-Arbeit jeder Quelldatei kann vollständig unabhängig erledigt werden. Wenn es beispielsweise 5000 Quelldateien und 8 CPUs gibt, können die Dateien in acht Teile aufgeteilt werden, und jede CPU verarbeitet einen Teil. Im gemeinsam genutzten Speicherraum wird nach Abschluss der Teil ausgeführt, der alle Datenstrukturen erstellt und verknüpft. Es ist sehr einfach, die Parallelverarbeitung der Parsing-Phase in Go zu implementieren. Es sind wahrscheinlich nur etwa 10 Zeilen Code erforderlich, um den Vorgang in einer Goroutine auszuführen, und gleichzeitig ein Mutex zu verwenden, um die gemeinsam genutzten Ressourcen zu schützen, was die Leistung um das 3- bis 4-fache verbessern kann.
(III) Das Parallelverarbeitungsschema für die Typüberprüfungsphase
Da der Typchecker eine globale Sicht des Programms benötigt, kann er nicht wie der Parsing-Prozess vollständig unabhängig sein. Das Team teilt das Programm in mehrere Teile auf (derzeit fest in vier Teile codiert) und erstellt vier Typchecker. Jeder Checker überprüft den zugewiesenen Teil der Dateien. Sie teilen sich den zugrunde liegenden unveränderlichen AST und erstellen ihre eigenen Typzustände. Obwohl diese Methode etwa 20 % mehr Speicher verbraucht (aufgrund von Typduplizierung), kann sie eine zusätzliche Leistungsverbesserung von etwa dem 3-fachen erzielen. In Kombination mit der 3-fachen Leistungsverbesserung durch den nativen Code kann die Gesamtleistungsverbesserung das 10-fache erreichen.
V. Zukunftsaussichten für TypeScript
(I) Der Entwicklungstrend der Sprachfunktionen
Derzeit hat sich die Entwicklungsgeschwindigkeit von ECMAScript verlangsamt. Das Feedback der Community zeigt, dass die Leute sich mehr um Skalierbarkeit und Leistung als um neue ausgefallene Funktionen des Typsystems kümmern. Das TypeScript-Team wird die Arbeit des ECMAScript-Komitees aufmerksam verfolgen, neue Funktionen im Typsystem ordnungsgemäß behandeln und gleichzeitig über die Auswirkungen einer 10-fachen Geschwindigkeitssteigerung des Typcheckers nachdenken und neue Möglichkeiten erkunden.
(II) Die Möglichkeit der Kombination mit künstlicher Intelligenz
Verwenden Sie den schnellen Typchecker, um Kontextinformationen für große Sprachmodelle (LLMs) bereitzustellen, wie z. B. Typauflösungsergebnisse, Speicherorte von Symboldeklarationen usw. Überprüfen Sie die Ausgabe von KI in Echtzeit, um ihre semantische Korrektheit sicherzustellen und eine Garantie dafür zu geben, dass KI sicheren und zuverlässigen Code generiert, wodurch neue Entwicklungspfade eröffnet werden.
(III) Die Konzeption einer nativen Laufzeitumgebung
Untersuchen Sie, ob eine native Laufzeitumgebung für TypeScript möglich ist. Derzeit gibt es Deno, das in Rust geschrieben ist. Obwohl es einige Faktoren in JavaScript gibt, die die Leistung beeinträchtigen, wie z. B. das Objektmodell und die Art der Zahlenverarbeitung, ist die Erstellung einer nativen Laufzeitumgebung für TypeScript mit vielen unsicheren Faktoren verbunden, und die zukünftige Entwicklungsrichtung ist noch unklar.
VI. Beiträge Dritter und Auswirkungen auf die Community
Der Übergang von JavaScript zu Go ist für das System relativ sanft. Obwohl es weniger Leute gibt, die sowohl Go als auch JavaScript kennen, verglichen mit denen, die nur JavaScript kennen, was zu einem Rückgang der Anzahl der Mitwirkenden führen kann, war die Anzahl der Leute, die zum Compiler beitragen, ursprünglich nicht groß, und sie sind normalerweise daran interessiert, in die native Umgebung einzutreten. Die Go-Sprache ist einfach, und ihr einfaches Design hat bemerkenswerte Ergebnisse wie eine 10-fache Leistungsverbesserung gebracht, was die Vitalität und Entwicklung der Community nicht behindern wird.
VII. Vergleich gängiger Anweisungen zwischen TypeScript und der Go-Sprache
(I) Schleifen
- TypeScript (basierend auf JavaScript)
for
-Schleife:
for (let i = 0; i < 10; i++) { console.log(i); }
for...of
-Schleife (wird verwendet, um über iterable Objekte wie Arrays zu iterieren):
const arr = [1, 2, 3]; for (const num of arr) { console.log(num); }
for...in
-Schleife (wird hauptsächlich verwendet, um über die Eigenschaften eines Objekts zu iterieren):
const obj = { a: 1, b: 2 }; for (const key in obj) { console.log(key, obj[key]); }
- Go-Sprache
for
-Schleife (die Go-Sprache hat nur eine grundlegende Schleifenstruktur, diefor
-Schleife, kann aber verschiedene Schleifenarten implementieren):
for i := 0; i < 10; i++ { fmt.Println(i) }
- Iterieren über iterable Objekte wie Arrays und Slices:
arr := []int{1, 2, 3} for index, value := range arr { fmt.Println(index, value) }
- Iterieren über eine Map:
m := map[string]int{"a": 1, "b": 2} for key, value := range m { fmt.Println(key, value) }
(II) Funktionen
- TypeScript
- Funktionsdefinition:
function add(a: number, b: number): number { return a + b; }
- Arrow-Funktion:
const multiply = (a: number, b: number): number => a * b;
- Go-Sprache
- Funktionsdefinition:
func add(a int, b int) int { return a + b }
- Anonyme Funktion (kann einer Variablen zugewiesen oder als Parameter übergeben werden):
multiply := func(a int, b int) int { return a * b }
(III) Objektorientierte Programmierung (OOP)
- TypeScript
- Klassendefinition:
class Animal { name: string; constructor(name: string) { this.name = name; } speak() { console.log(`${this.name} makes a sound.`); } }
- Vererbung:
class Dog extends Animal { constructor(name: string) { super(name); } speak() { console.log(`${this.name} barks.`); } }
- Go-Sprache
- Die Go-Sprache hat nicht die traditionellen Konzepte von Klassen und Vererbung. Sie erreicht Funktionen, die OOP ähneln, durch Structs und Methodensets.
- Struct-Definition:
type Animal struct { Name string } func (a *Animal) Speak() { fmt.Printf("%s makes a sound.\n", a.Name) }
- Realisieren Sie etwas Ähnliches wie Vererbung durch Komposition:
type Dog struct { Animal } func (d *Dog) Speak() { fmt.Printf("%s barks.\n", d.Name) }
(IV) Funktionale Programmierung
- TypeScript
- Beispiel einer Higher-Order-Funktion (akzeptiert eine Funktion als Parameter):
function operateOnArray(arr: number[], callback: (num: number) => number): number[] { return arr.map(callback); } const result = operateOnArray([1, 2, 3], num => num * 2);
- Unveränderliche Datenstrukturen können mit Hilfe externer Bibliotheken implementiert werden (z. B. Immutable.js). Zum Beispiel:
import { fromJS } from 'immutable'; const list = fromJS([1, 2, 3]); const newList = list.push(4);
- Go-Sprache
- Beispiel einer Higher-Order-Funktion:
func operateOnSlice(slice []int, callback func(int) int) []int { result := make([]int, len(slice)) for i, v := range slice { result[i] = callback(v) } return result } result := operateOnSlice([]int{1, 2, 3}, func(num int) int { return num * 2 })
- Die Go-Sprache selbst unterstützt nativ keine unveränderlichen Datenstrukturen wie einige funktionale Programmiersprachen. Unveränderliches Verhalten kann jedoch durch einige Entwurfsmuster und Bibliotheken simuliert werden, z. B. durch Sicherstellung der Datenunveränderlichkeit durch das Kopieren von Structs und anderen Methoden.
Referenz: https://www.youtube.com/watch?v=pNlq-EVld70&ab_channel=MicrosoftDeveloper
Leapcell: Die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis
Schließlich möchte ich die am besten geeignete Plattform für die Bereitstellung von Go-Diensten empfehlen: Leapcell
1. Multi-Sprachen-Unterstützung
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
2. Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung — keine Anfragen, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 $ unterstützt 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und Protokollierung für umsetzbare Erkenntnisse.
5. Mühelose Skalierbarkeit und hohe Leistung
- Auto-Skalierung zur einfachen Bewältigung hoher Parallelität.
- Keine operativen Gemeinkosten — konzentrieren Sie sich einfach auf das Bauen.
Erfahren Sie mehr in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ