Maps
Je kunt hier alle code van dit hoofdstuk vinden
In arrays en slices heb je gezien hoe je waarden in volgorde kunt opslaan. Nu gaan we kijken naar een manier om items op sleutel op te slaan en ze snel op te zoeken.
Met Maps kun je items opslaan op een manier die vergelijkbaar is met een woordenboek. Je kunt de key zien als het woord en de value als de definitie. En wat is er nou beter dan Maps te leren kennen door je eigen woordenboek te bouwen?
Ten eerste, ervan uitgaande dat er al een aantal woorden met hun definities in het woordenboek staan, zou het woordenboek, als we naar een woord zoeken, de definitie ervan moeten teruggeven.
Schrijf eerst je test
In dictionary_test.go
package main
import "testing"
func TestSearch(t *testing.T) {
dictionary := map[string]string{"test": "this is just a test"}
got := Search(dictionary, "test")
want := "this is just a test"
if got != want {
t.Errorf("got %q want %q given, %q", got, want, "test")
}
}Het declareren van een map lijkt enigszins op een array. Behalve dat het begint met het sleutelwoord map en twee typen vereist. Het eerste is het sleuteltype, dat tussen de [] staat. Het tweede is het waardetype, dat direct na de [] komt.
Het sleuteltype is speciaal. Het kan alleen een vergelijkbaar type zijn, want zonder de mogelijkheid om te bepalen of twee sleutels gelijk zijn, kunnen we niet garanderen dat we de juiste waarde krijgen. Vergelijkbare typen worden uitgebreid uitgelegd in de taalspecificatie.
Het waardetype kan daarentegen elk gewenst type zijn. Het kan zelfs een andere map zijn.
Al het overige in de test zou je bekend voor moeten komen.
Probeer de test uit te voeren
Door het uitvoeren van go test zal de compiler falen met ./dictionary_test.go:8:9: undefined: Search.
Schrijf de minimale hoeveelheid code om de test te laten uitvoeren en de falende test output te controleren
In dictionary.go
Je test zou nu moeten mislukken met een duidelijke foutmelding
dictionary_test.go:12: got '' want 'this is just a test' given, 'test'.
Schrijf genoeg code om de test te laten slagen
Het ophalen van een waarde uit een Map is hetzelfde als het ophalen van een waarde uit een Array map[key].
Refactor
Ik heb besloten een assertStrings-helper te maken om de implementatie algemener te maken.
Maak gebruik van een custom type
We kunnen het gebruik van ons woordenboek verbeteren door een nieuw type rondom map te creëren en van Search een methode te maken.
In dictionary_test.go:
Hier maken we gebruik van het Dictionary-type, dat nog niet gedefinieerd is. Vervolgens roepen we Search aan op de Dictionary-instantie.
We hoeven hiervoor niet de assertStrings-helper aan te passen.
In dictionary.go:
Hier hebben we een woordenboektype gemaakt dat als een dunne wrapper rond de map fungeert. Met het aangepaste type gedefinieerd, kunnen we de Search methode maken.
Schrijf eerst je test
De basis zoekfunctie was heel eenvoudig te implementeren, maar wat gebeurt er als we een woord invoeren dat niet in ons woordenboek voorkomt?
We krijgen eigenlijk niets terug. Dit is goed, omdat het programma gewoon door kan blijven draaien, maar er is een betere aanpak. De functie kan melden dat het woord niet in het woordenboek staat. Zo hoeft de gebruiker zich niet af te vragen of het woord niet bestaat of dat er gewoon geen definitie is (dit lijkt misschien niet erg nuttig voor een woordenboek. Het is echter een scenario dat in andere gevallen cruciaal zou kunnen zijn).
De manier om dit scenario in Go af te handelen is om een ​​tweede argument te retourneren, namelijk van het type Error.
Merk op dat we in de sectie over pointers en errors hebben gezien dat we, om de foutmelding te bevestigen, eerst moeten controleren of de fout niet nil is. Vervolgens gebruiken we de methode .Error() om de tekenreeks te verkrijgen die we vervolgens aan de bevestiging kunnen doorgeven.
Probeer de test uit te voeren
Dit zal niet compileren
Schrijf de minimale hoeveelheid code om de test te laten uitvoeren en de falende test output te controleren
Je test zou nu moeten mislukken en er verschijnt een veel duidelijkere foutmelding.
dictionary_test.go:22: expected to get an error.
Schrijf genoeg code om de test te laten slagen
Om dit mogelijk te maken, gebruiken we een interessante eigenschap van de map lookup. Deze kan twee waarden retourneren. De tweede waarde is een boolean die aangeeft of de sleutel succesvol is gevonden.
Dankzij deze eigenschap kunnen we onderscheid maken tussen een woord dat niet bestaat en een woord dat geen definitie heeft.
Refactor
We kunnen de magische fout in onze Search-functie verwijderen door deze in een variabele te extraheren. Dit stelt ons ook in staat om een ​​betere test te doen.
Door een nieuwe helper te maken, konden we onze test vereenvoudigen en zijn we onze variabele ErrNotFound gaan gebruiken. Hierdoor mislukt de test niet meer als we in de toekomst de fouttekst wijzigen.
Schrijf eerst je teste
We hebben een geweldige manier om het woordenboek te doorzoeken. We kunnen echter geen nieuwe woorden aan ons woordenboek toevoegen.
In deze test maken we gebruik van onze Search functie om de validatie van het woordenboek iets eenvoudiger te maken.
Schrijf de minimale hoeveelheid code om de test te laten uitvoeren en de falende test output te controleren
In dictionary.go
Je test zou nu moeten falen:
Schrijf genoeg code om test te laten slagen
Toevoegen aan een map is vergelijkbaar met een array. Je hoeft alleen een sleutel op te geven en deze gelijk te stellen aan een waarde.
Pointers, kopieën, etc
Een interessante eigenschap van maps is dat je ze kunt aanpassen zonder dat je er een adres aan hoeft door te geven (bijvoorbeeld &myMap)
Daardoor voelen ze misschien aan als een 'referentietype', maar zoals Dave Cheney het beschrijft, zijn ze dat niet.
Een mapwaarde is een aanwijzer naar een runtime.hmap-structuur.
Wanneer je dus een map doorgeeft aan een functie/methode, kopieer je deze feitelijk, maar alleen het pointer-gedeelte, niet de onderliggende gegevensstructuur die de data bevat.
Een valkuil bij maps is dat ze een nil-waarde kunnen hebben. Een nil-map gedraagt ​​zich tijdens het lezen als een lege map, maar pogingen om naar een nil-map te schrijven veroorzaken runtime-panic. Je kunt hier meer over maps lezen.
Daarom mag u nooit een nil map-variabele initialiseren:
In plaats daarvan kun je een lege map initialiseren of het trefwoord make gebruiken om een ​​map voor je te maken:
Beide benaderingen creëren een lege hashmap en verwijzen dictionary ernaar. Dit zorgt ervoor dat je nooit een runtime-panic krijgt.
Refactor
Er valt niet veel te refactoren in onze implementatie, maar de test zou wel wat vereenvoudigd kunnen.
We hebben variabelen voor woord en definitie gemaakt en de definitie-vergelijking naar een eigen hulpfunctie verplaatst.
Onze Add ziet er goed uit. Alleen hebben we niet nagedacht over wat er gebeurt als de waarde die we proberen toe te voegen al bestaat!
Map geeft geen foutmelding als de waarde al bestaat. In plaats daarvan overschrijven ze de waarde met de nieuw opgegeven waarde. Dit kan in de praktijk handig zijn, maar maakt onze functienaam minder nauwkeurig. Add mag bestaande waarden niet wijzigen. Het mag alleen nieuwe woorden aan ons woordenboek toevoegen.
Schrijf eerst je test
Voor deze test hebben we Add aangepast om een ​​fout te retourneren, die we valideren met een nieuwe foutvariabele, ErrWordExists. We hebben ook de vorige test aangepast om te controleren op een nulfout.
Probeer de test uit te voeren
De compiler zal falen omdat we geen waarde voor Add retourneren.
Schrijf de minimale hoeveelheid code om de test te laten uitvoeren en de falende test output te controleren
In dictionary.go
Nu krijgen we nog twee fouten. We zijn de waarde nog steeds aan het aanpassen en retourneren een nil-fout.
Schrijf genoeg code om de test te laten slagen
Hier gebruiken we een switch-statement om de fout te matchen. Een dergelijke switch biedt een extra vangnet voor het geval Search een andere fout dan ErrNotFound retourneert.
Refactor
Er valt niet heel veel te refactoren, maar naarmate het aantal fout-afhandelingen toeneemt, kunnen we een paar aanpassingen doen.
We hebben de fout-meldingen constant gemaakt; hiervoor moesten we ons eigen DictionaryErr-type maken dat de errorinterface implementeert. Je kunt meer over de details lezen in dit uitstekende artikel van Dave Cheney. Simpel gezegd: het maakt de fouten herbruikbaarder en onveranderlijker.
Laten we nu een functie maken om de definitie van een woord bij te werken.
Schrijf eerste je test
Update is nauw verwant aan Add en zal onze volgende implementatie zijn.
Probeer de test uit te voeren
Schrijf de minimale hoeveelheid code om de test te laten uitvoeren en de falende test output te controleren
We weten al hoe we met een dergelijke fout moeten omgaan. We moeten onze functie definiëren.
Nu we dat weten, zien we dat we de definitie van het woord moeten veranderen.
Schrijf genoeg code om de test te laten slagen
We hebben al gezien hoe we dit kunnen doen toen we het probleem met Add oplosten. Laten we dus iets implementeren dat erg lijkt op Add.
We hoeven hier geen refactoring op uit te voeren, aangezien het een eenvoudige wijziging was. We hebben nu echter hetzelfde probleem als met Add. Als we een nieuw woord doorgeven, voegt Update het toe aan het woordenboek.
Schrijf eerst je test
We hebben nog een fouttype toegevoegd voor wanneer het woord niet bestaat. Ook hebben we Update aangepast om een ​​errorwaarde te retourneren.
Probeer de test uit te voeren
Deze keer krijgen we 3 fouten, maar we weten hoe we daarmee om moeten gaan.
Schrijf de minimale hoeveelheid code om de test te laten uitvoeren en de falende test output te controleren
We hebben ons eigen fouttype toegevoegd en retourneren een nil-fout.
Door deze wijzigingen krijgen we nu een heel duidelijke foutmelding:
Schrijf genoeg code om de test te laten slagen
Deze functie lijkt bijna hetzelfde als Add, behalve dat we we nu een update doen in de dictionary wanneer we geen fout krijgen.
Opmerking over het declareren van een nieuwe fout voor Update
We zouden ErrNotFound kunnen hergebruiken zonder een nieuwe fout toe te voegen. Het is echter vaak beter om een ​​specifieke foutmelding te hebben voor wanneer een update mislukt.
Door specifieke fouten te hebben, krijg je meer informatie over wat er misgaat. Hier is een voorbeeld in een webapp:
Je kunt de gebruiker omleiden wanneer
ErrNotFoundwordt gegeven, maar een foutmelding tonen wanneerErrWordDoesNotExistwordt gegeven.
Laten we nu een functie maken om een ​​woord uit het woordenboek te verwijderen.
Schrijf je eerste test
Onze test maakt een Dictionary met een woord en controleert vervolgens of het woord is verwijderd.
Probeer de test uit te voeren
Door go test uit te voeren, krijgen we:
Schrijf de minimale hoeveelheid code om de test te laten uitvoeren en de falende test output te controleren
Nadat we dit hebben toegevoegd, geeft de test aan dat we het woord niet verwijderd is.
Schrijf genoeg code om de test te laten slagen
Go heeft een ingebouwde functie delete die werkt op maps. Deze functie accepteert twee argumenten en retourneert niets. Het eerste argument is de map en het tweede de te verwijderen sleutel.
Refactor
Er valt niet veel te refactoren, maar we kunnen dezelfde logica uit Update implementeren om gevallen af ​​te handelen waarin het woord niet bestaat.
Voer de test uit
De compiler zal falen omdat we geen waarde voor Delete retourneren.
Schrijf genoeg code om test te laten slagen
We gebruiken opnieuw een switch-instructie om de fout te detecteren die optreedt wanneer we een woord proberen te verwijderen dat niet bestaat.
Samenvattend
In deze sectie hebben we veel behandeld. We hebben een volledige CRUD (Create, Read, Update en Delete) API voor ons woordenboek gemaakt. Tijdens dit proces hebben we geleerd hoe we:
Maps aan kunnen maken
Kunnen zoeken naar items binnen die maps
Nieuwe items aan maps toe kunnen voegen
Items in een map kunnen updaten
Items van een map kunnen verwijderen
Geleerd over errors
Hoe we errors kunnen maken als constanten
Hoe we error wrappers kunnen schrijven
Laatst bijgewerkt